mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
1473 lines
50 KiB
TypeScript
1473 lines
50 KiB
TypeScript
import {
|
|
semanticBindingMayHaveOpaqueEffect,
|
|
semanticBindingCanBeRebound,
|
|
semanticOpaqueBindingReadMayHaveChanged,
|
|
} from "./javascriptSemanticOpaqueEffects.js";
|
|
import * as t from "@babel/types";
|
|
import { logicalRightOperand } from "./javascriptSymbolicControlFlow.js";
|
|
|
|
import {
|
|
SemanticDependencyProofs,
|
|
type SemanticDependencyProof,
|
|
} from "./javascriptSemanticDependencies.js";
|
|
|
|
import {
|
|
semanticCapturePosition,
|
|
semanticEscapeFollowsCapture,
|
|
semanticBindingPrecedesCapture,
|
|
semanticBindingAtRead,
|
|
} from "./javascriptSemanticMutationInitializers.js";
|
|
|
|
import {
|
|
semanticSlotAtPath,
|
|
semanticContainer,
|
|
} from "./javascriptSemanticSlots.js";
|
|
|
|
import {
|
|
invalidateSemanticEscapedPath,
|
|
invalidateSemanticMutationPaths,
|
|
} from "./javascriptSemanticMutationValues.js";
|
|
|
|
import type { JavaScriptBindingProvenance } from "./javascriptSemanticIr.js";
|
|
import type {
|
|
JavaScriptSemanticProperty,
|
|
JavaScriptSemanticResourceLimit,
|
|
JavaScriptSemanticValue,
|
|
} from "./javascriptSemanticValueTypes.js";
|
|
import {
|
|
recordSemanticValueLimit,
|
|
resolveSemanticBindingState,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import {
|
|
readExactJavaScriptLiteral,
|
|
semanticStaticPropertyKey,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import {
|
|
semanticAmbiguousProvenance,
|
|
semanticLocalProvenance,
|
|
semanticOriginsProvenance,
|
|
semanticUnresolvedProvenance,
|
|
uniqueSemanticOrigins,
|
|
} from "./javascriptSemanticProvenance.js";
|
|
import {
|
|
semanticPrimitiveCandidates as primitiveCandidates,
|
|
semanticPrimitiveSet as primitiveSet,
|
|
} from "./javascriptSemanticPrimitives.js";
|
|
import {
|
|
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
|
|
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
|
|
exceedsSemanticPrimitiveAdditionByteBudget,
|
|
exceedsSemanticTemplateByteBudget,
|
|
semanticResourceLimitUnknown,
|
|
semanticResourceLimitReason,
|
|
} from "./javascriptSemanticResourceLimits.js";
|
|
|
|
// Function identity is outside the public constant lattice. Retain its
|
|
// non-undefined proof with the immutable value rather than interpreting an
|
|
// "unknown" reason or recovering callable syntax in default consumers.
|
|
const definitelyDefinedValues = new WeakSet<JavaScriptSemanticValue>();
|
|
|
|
const definedNonPrimitiveValue = (reason: string): JavaScriptSemanticValue => {
|
|
const value: JavaScriptSemanticValue = { status: "unknown", reason };
|
|
definitelyDefinedValues.add(value);
|
|
return value;
|
|
};
|
|
|
|
/** Definedness is independent of whether the constant lattice retains a value. */
|
|
export const semanticValueDefinitelyDefined = (
|
|
value: JavaScriptSemanticValue,
|
|
): boolean =>
|
|
value.status === "literal" ||
|
|
value.status === "union" ||
|
|
value.status === "object" ||
|
|
value.status === "array" ||
|
|
definitelyDefinedValues.has(value);
|
|
|
|
interface MemoizedEvaluation<T> {
|
|
readonly value: T;
|
|
readonly dependencies: SemanticDependencyProof;
|
|
}
|
|
|
|
interface ReadSnapshotValue extends MemoizedEvaluation<JavaScriptSemanticValue> {
|
|
readonly depthDependent: boolean;
|
|
}
|
|
|
|
type ReadSnapshotValues = WeakMap<
|
|
JavaScriptSemanticBindingState["initializers"][number],
|
|
Map<string, Map<number, ReadSnapshotValue>>
|
|
>;
|
|
|
|
/** Read/provenance memos live for one query; whole values follow state effects. */
|
|
interface EvaluationMemo {
|
|
snapshotValues?: ReadSnapshotValues;
|
|
capturedSnapshotValues?: WeakMap<t.Node, ReadSnapshotValues>;
|
|
readonly provenance: Map<
|
|
string,
|
|
Map<number, MemoizedEvaluation<JavaScriptBindingProvenance>>
|
|
>;
|
|
readonly dependencyFrames: { proof: SemanticDependencyProof }[];
|
|
readonly dependencies: SemanticDependencyProofs;
|
|
}
|
|
|
|
// Indexing owns stable binding identities; effects invalidate values, not this
|
|
// identity-only layout. No query result or dependency closure is retained here.
|
|
const dependencyProofsByState = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
SemanticDependencyProofs
|
|
>();
|
|
|
|
const createEvaluationMemo = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): EvaluationMemo => {
|
|
let dependencies = dependencyProofsByState.get(state);
|
|
if (dependencies === undefined) {
|
|
dependencies = new SemanticDependencyProofs(state.bindingsById.keys());
|
|
dependencyProofsByState.set(state, dependencies);
|
|
}
|
|
return { provenance: new Map(), dependencyFrames: [], dependencies };
|
|
};
|
|
|
|
interface EvaluationContext {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
/** Bindings under evaluation, by their position on the active stack. */
|
|
readonly bindings: Map<string, number>;
|
|
readonly expressionDepth: number;
|
|
readonly capturePoint?: t.Node;
|
|
readonly primitiveBindingValues: Map<string, JavaScriptSemanticValue>;
|
|
readonly dependence: EvaluationDependence;
|
|
readonly memo: EvaluationMemo;
|
|
}
|
|
|
|
/**
|
|
* How far up the active stack the value being evaluated depends on its
|
|
* caller. An alias cycle depends on the position of its head; the expression
|
|
* depth budget depends on every caller. A binding whose value depends only on
|
|
* positions pushed after its own evaluates to that value from any caller
|
|
* with at least as much of the depth budget left.
|
|
*/
|
|
interface EvaluationDependence {
|
|
lowest: number;
|
|
}
|
|
|
|
const CALLER_INDEPENDENT = Number.POSITIVE_INFINITY;
|
|
const EVERY_CALLER = -1;
|
|
|
|
/** A value no caller changes, and the deepest evaluation known to yield it. */
|
|
interface CallerIndependentBindingValue extends MemoizedEvaluation<JavaScriptSemanticValue> {
|
|
readonly expressionDepth: number;
|
|
}
|
|
|
|
const callerIndependentBindingValuesByState = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
Map<string, CallerIndependentBindingValue>
|
|
>();
|
|
|
|
const primitiveBindingValuesByState = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
Map<string, JavaScriptSemanticValue>
|
|
>();
|
|
|
|
const primitiveExpressionValuesByState = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
WeakMap<t.Node, JavaScriptSemanticValue>
|
|
>();
|
|
|
|
const capturedBindingValuesByState = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
WeakMap<t.Node, Map<string, JavaScriptSemanticValue>>
|
|
>();
|
|
|
|
/** Discard every state-owned value derived before the effects boundary changed. */
|
|
export const clearSemanticValueCaches = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
primitiveBindingValuesByState.delete(state);
|
|
capturedBindingValuesByState.delete(state);
|
|
primitiveExpressionValuesByState.delete(state);
|
|
callerIndependentBindingValuesByState.delete(state);
|
|
};
|
|
|
|
const callerIndependentBindingValuesFor = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): Map<string, CallerIndependentBindingValue> => {
|
|
const existing = callerIndependentBindingValuesByState.get(state);
|
|
if (existing !== undefined) return existing;
|
|
const values = new Map<string, CallerIndependentBindingValue>();
|
|
callerIndependentBindingValuesByState.set(state, values);
|
|
return values;
|
|
};
|
|
|
|
const primitiveBindingValuesFor = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): Map<string, JavaScriptSemanticValue> => {
|
|
const existing = primitiveBindingValuesByState.get(state);
|
|
if (existing !== undefined) return existing;
|
|
const values = new Map<string, JavaScriptSemanticValue>();
|
|
primitiveBindingValuesByState.set(state, values);
|
|
return values;
|
|
};
|
|
|
|
// 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 =>
|
|
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 = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
capturePoint?: t.Node,
|
|
): JavaScriptSemanticValue => {
|
|
const context: EvaluationContext = {
|
|
state,
|
|
bindings: new Map(),
|
|
expressionDepth: 0,
|
|
primitiveBindingValues: primitiveBindingValuesFor(state),
|
|
dependence: { lowest: CALLER_INDEPENDENT },
|
|
memo: createEvaluationMemo(state),
|
|
};
|
|
return evaluateExpression(
|
|
node,
|
|
capturePoint === undefined
|
|
? context
|
|
: (captureContextAt(capturePoint, context) ?? context),
|
|
);
|
|
};
|
|
|
|
/** Resolve an expression using the same alias and module provenance owner. */
|
|
export const evaluateSemanticExpressionProvenance = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptBindingProvenance =>
|
|
provenanceForExpression(node, {
|
|
state,
|
|
bindings: new Map(),
|
|
expressionDepth: 0,
|
|
primitiveBindingValues: primitiveBindingValuesFor(state),
|
|
dependence: { lowest: CALLER_INDEPENDENT },
|
|
memo: createEvaluationMemo(state),
|
|
});
|
|
|
|
/** Follow module provenance through destructuring, members, and aliases. */
|
|
export const evaluateSemanticProvenance = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptBindingProvenance => {
|
|
const provenance = provenanceForBinding(binding, {
|
|
state,
|
|
bindings: new Map(),
|
|
expressionDepth: 0,
|
|
primitiveBindingValues: primitiveBindingValuesFor(state),
|
|
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 = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
read: t.Identifier,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptBindingProvenance =>
|
|
provenanceForBindingAtRead(binding, read, {
|
|
state,
|
|
bindings: new Map(),
|
|
expressionDepth: 0,
|
|
primitiveBindingValues: primitiveBindingValuesFor(state),
|
|
dependence: { lowest: CALLER_INDEPENDENT },
|
|
memo: createEvaluationMemo(state),
|
|
});
|
|
|
|
const readMemoizedEvaluation = <T>(
|
|
entry: MemoizedEvaluation<T> | undefined,
|
|
context: EvaluationContext,
|
|
): T | undefined => {
|
|
if (
|
|
entry === undefined ||
|
|
context.memo.dependencies.intersects(entry.dependencies, context.bindings)
|
|
)
|
|
return undefined;
|
|
retainDependencyProof(entry.dependencies, context);
|
|
return entry.value;
|
|
};
|
|
|
|
const retainDependencyProof = (
|
|
proof: SemanticDependencyProof,
|
|
context: EvaluationContext,
|
|
): void => {
|
|
const parent = context.memo.dependencyFrames.at(-1);
|
|
if (parent !== undefined)
|
|
parent.proof = context.memo.dependencies.union(parent.proof, proof);
|
|
};
|
|
|
|
const recordBindingDependency = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): void =>
|
|
retainDependencyProof(
|
|
context.memo.dependencies.binding(binding.bindingId),
|
|
context,
|
|
);
|
|
|
|
const collectEvaluationDependencies = <T>(
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
evaluate: () => T,
|
|
): MemoizedEvaluation<T> => {
|
|
const frame = { proof: context.memo.dependencies.binding(binding.bindingId) };
|
|
context.memo.dependencyFrames.push(frame);
|
|
try {
|
|
return { value: evaluate(), dependencies: frame.proof };
|
|
} finally {
|
|
context.memo.dependencyFrames.pop();
|
|
retainDependencyProof(frame.proof, context);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A value snapshot consumes its sole initializer plus its source binding's
|
|
* effects. Key the parsed initializer fact (including projection), binding ID
|
|
* and capture point, rather than an ephemeral snapshot object or the whole ID.
|
|
* Query ownership prevents reuse across effect changes. Exact expression-depth
|
|
* keys also bound failed speculative evaluations without skipping their budget.
|
|
*/
|
|
const readSnapshotValuesFor = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): Map<number, ReadSnapshotValue> | undefined => {
|
|
const initializer = binding.initializers[0];
|
|
if (binding.initializers.length !== 1 || initializer === undefined)
|
|
return undefined;
|
|
let values: ReadSnapshotValues;
|
|
if (context.capturePoint === undefined)
|
|
values = context.memo.snapshotValues ??= new WeakMap();
|
|
else {
|
|
const captures = (context.memo.capturedSnapshotValues ??= new WeakMap());
|
|
let captured = captures.get(context.capturePoint);
|
|
if (captured === undefined) {
|
|
captured = new WeakMap();
|
|
captures.set(context.capturePoint, captured);
|
|
}
|
|
values = captured;
|
|
}
|
|
let entries = values.get(initializer);
|
|
if (entries === undefined) {
|
|
entries = new Map();
|
|
values.set(initializer, entries);
|
|
}
|
|
let depths = entries.get(binding.bindingId);
|
|
if (depths === undefined) {
|
|
depths = new Map();
|
|
entries.set(binding.bindingId, depths);
|
|
}
|
|
return depths;
|
|
};
|
|
|
|
const evaluateBinding = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
// Temporal read facts use a query-owned cache separate from whole bindings.
|
|
wholeBinding = true,
|
|
): JavaScriptSemanticValue => {
|
|
recordBindingDependency(binding, context);
|
|
const cycleHead = context.bindings.get(binding.bindingId);
|
|
if (cycleHead !== undefined) {
|
|
context.dependence.lowest = Math.min(context.dependence.lowest, cycleHead);
|
|
return { status: "cycle", reason: `Alias cycle at ${binding.name}.` };
|
|
}
|
|
if (
|
|
wholeBinding &&
|
|
context.capturePoint !== undefined &&
|
|
!semanticBindingPrecedesCapture(
|
|
binding,
|
|
context.capturePoint,
|
|
context.state.parentsByNode,
|
|
)
|
|
) {
|
|
const { capturePoint: _capturePoint, ...liveContext } = context;
|
|
return evaluateBinding(binding, {
|
|
...liveContext,
|
|
primitiveBindingValues: primitiveBindingValuesFor(context.state),
|
|
});
|
|
}
|
|
const cached = wholeBinding
|
|
? context.primitiveBindingValues.get(binding.bindingId)
|
|
: undefined;
|
|
if (cached !== undefined) return cached;
|
|
// Each closure that aliases an outer binding evaluates it both live and at
|
|
// its capture point, so re-deriving it doubles the work at every level of
|
|
// nesting. A deeper reader derives it again: the retained value would carry
|
|
// more nesting than the remaining depth budget allows it to observe.
|
|
const snapshots = wholeBinding
|
|
? undefined
|
|
: readSnapshotValuesFor(binding, context);
|
|
const snapshot = snapshots?.get(context.expressionDepth);
|
|
const snapshotValue = readMemoizedEvaluation(snapshot, context);
|
|
if (snapshotValue !== undefined) {
|
|
// A result limited by this budget must not become caller-independent when
|
|
// reused inside another binding evaluated at the same remaining depth.
|
|
if (snapshot?.depthDependent === true)
|
|
context.dependence.lowest = EVERY_CALLER;
|
|
return snapshotValue;
|
|
}
|
|
const independent =
|
|
wholeBinding && context.capturePoint === undefined
|
|
? callerIndependentBindingValuesFor(context.state)
|
|
: undefined;
|
|
const retained = independent?.get(binding.bindingId);
|
|
if (
|
|
retained !== undefined &&
|
|
context.expressionDepth <= retained.expressionDepth
|
|
) {
|
|
const reusable = readMemoizedEvaluation(retained, context);
|
|
if (reusable !== undefined) return reusable;
|
|
}
|
|
if (
|
|
binding.referenceInitializers.some(({ assignment }) => assignment) &&
|
|
(binding.initializers.length > 0 ||
|
|
(context.state.bindingsById.get(binding.bindingId)?.initializers.length ??
|
|
0) > 0)
|
|
)
|
|
return {
|
|
status: "ambiguous",
|
|
reason: `Binding ${binding.name} has value and projected-reference assignments.`,
|
|
};
|
|
if (binding.initializers.length === 0)
|
|
return {
|
|
status: "unknown",
|
|
reason: wholeBinding
|
|
? `Binding ${binding.name} has no constant initializer.`
|
|
: `Binding ${binding.name} has no proven initializer at this read.`,
|
|
};
|
|
if (
|
|
binding.initializers.some(
|
|
({ node }) =>
|
|
t.isUpdateExpression(node) ||
|
|
(t.isAssignmentExpression(node) &&
|
|
node.operator !== "=" &&
|
|
node.operator !== "&&=" &&
|
|
node.operator !== "||=" &&
|
|
node.operator !== "??="),
|
|
)
|
|
)
|
|
return {
|
|
status: "unknown",
|
|
reason: `Binding ${binding.name} has a compound or update write.`,
|
|
};
|
|
if (binding.initializers.length > 1)
|
|
return {
|
|
status: "ambiguous",
|
|
reason: `Binding ${binding.name} has multiple possible assignments.`,
|
|
};
|
|
const initializer = binding.initializers[0];
|
|
if (initializer === undefined)
|
|
return { status: "unknown", reason: "Missing binding initializer." };
|
|
if (context.state.conditionallySkippedExpressions.has(initializer.node))
|
|
return {
|
|
status: "unknown",
|
|
reason: `Binding ${binding.name} may remain uninitialized.`,
|
|
};
|
|
const position = context.bindings.size;
|
|
const callerDependence = context.dependence.lowest;
|
|
context.dependence.lowest = CALLER_INDEPENDENT;
|
|
context.bindings.set(binding.bindingId, position);
|
|
let value: JavaScriptSemanticValue;
|
|
let callerIndependent = false;
|
|
let depthDependent = false;
|
|
let evaluated: MemoizedEvaluation<JavaScriptSemanticValue>;
|
|
try {
|
|
evaluated = collectEvaluationDependencies(binding, context, () => {
|
|
const nested = nestedContext(context);
|
|
let value = projectValue(
|
|
evaluateExpression(initializer.node, nested),
|
|
initializer.projection,
|
|
);
|
|
if (canCapturePrimitive(value)) {
|
|
const captureContext = primitiveCaptureContext(
|
|
initializer.node,
|
|
nested,
|
|
);
|
|
if (captureContext !== undefined) {
|
|
const captured = projectValue(
|
|
evaluateExpression(initializer.node, captureContext),
|
|
initializer.projection,
|
|
);
|
|
if (isPrimitive(captured)) value = captured;
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
value = evaluated.value;
|
|
// A cycle among bindings this one reads is part of its value. A cycle
|
|
// through this binding is cut elsewhere when a caller holds another member.
|
|
callerIndependent = context.dependence.lowest > position;
|
|
depthDependent = context.dependence.lowest === EVERY_CALLER;
|
|
} finally {
|
|
context.bindings.delete(binding.bindingId);
|
|
context.dependence.lowest = callerIndependent
|
|
? callerDependence
|
|
: Math.min(callerDependence, context.dependence.lowest);
|
|
}
|
|
let projected = invalidateSemanticMutationPaths(
|
|
value,
|
|
binding.memberWrites.map(({ path }) => path),
|
|
);
|
|
for (const escape of binding.escapedPaths) {
|
|
// Escapes only invalidate object references; other lattice values survive.
|
|
if (projected.status !== "object" && projected.status !== "array") break;
|
|
if (
|
|
context.capturePoint !== undefined &&
|
|
semanticEscapeFollowsCapture(
|
|
binding,
|
|
context.capturePoint,
|
|
escape.node,
|
|
context.state.parentsByNode,
|
|
)
|
|
)
|
|
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 &&
|
|
(retained === undefined ||
|
|
retained.expressionDepth < context.expressionDepth)
|
|
)
|
|
independent.set(binding.bindingId, {
|
|
value: projected,
|
|
dependencies: evaluated.dependencies,
|
|
expressionDepth: context.expressionDepth,
|
|
});
|
|
// A cycle through an active caller is not reusable. Depth-dependent partial
|
|
// values are reusable at this exact budget, with their dependence preserved.
|
|
if (
|
|
snapshots !== undefined &&
|
|
(callerIndependent || depthDependent) &&
|
|
!context.memo.dependencies.intersects(
|
|
evaluated.dependencies,
|
|
context.bindings,
|
|
)
|
|
)
|
|
snapshots.set(context.expressionDepth, {
|
|
value: projected,
|
|
dependencies: evaluated.dependencies,
|
|
depthDependent,
|
|
});
|
|
// Point-local memoization also bounds failed speculative captures. Only a
|
|
// primitive result is allowed back into ordinary value evaluation.
|
|
if (
|
|
wholeBinding &&
|
|
(context.capturePoint !== undefined ||
|
|
projected.status === "literal" ||
|
|
projected.status === "union" ||
|
|
(isSemanticResourceLimit(projected) &&
|
|
(projected.resourceLimit === "primitive-bytes" ||
|
|
projected.resourceLimit === "primitive-candidates")))
|
|
)
|
|
context.primitiveBindingValues.set(binding.bindingId, projected);
|
|
return projected;
|
|
};
|
|
|
|
const evaluateExpression = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
const value = evaluateExpressionValue(node, context);
|
|
recordSemanticValueLimit(context.state, value);
|
|
return value;
|
|
};
|
|
|
|
const evaluateExpressionValue = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
|
context.dependence.lowest = EVERY_CALLER;
|
|
return semanticResourceLimitUnknown("expression-depth");
|
|
}
|
|
const unwrapped = unwrapJavaScriptExpression(node);
|
|
if (unwrapped.depth > 0) {
|
|
if (
|
|
context.expressionDepth + unwrapped.depth >
|
|
SEMANTIC_EXPRESSION_DEPTH_LIMIT
|
|
) {
|
|
context.dependence.lowest = EVERY_CALLER;
|
|
return semanticResourceLimitUnknown("expression-depth");
|
|
}
|
|
return evaluateExpression(unwrapped.node, {
|
|
...context,
|
|
expressionDepth: context.expressionDepth + unwrapped.depth,
|
|
});
|
|
}
|
|
const literal = readExactJavaScriptLiteral(node);
|
|
if (literal.found) return primitiveSet([literal.value]);
|
|
if (
|
|
t.isFunctionExpression(node) ||
|
|
t.isFunctionDeclaration(node) ||
|
|
t.isArrowFunctionExpression(node) ||
|
|
t.isClassExpression(node) ||
|
|
t.isClassDeclaration(node)
|
|
)
|
|
return definedNonPrimitiveValue(`Unsupported ${node.type} value.`);
|
|
if (t.isIdentifier(node)) {
|
|
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
|
if (binding === undefined)
|
|
return { status: "unknown", reason: `Unbound identifier ${node.name}.` };
|
|
const read = semanticBindingAtRead(
|
|
binding,
|
|
node,
|
|
context.state.parentsByNode,
|
|
);
|
|
if (
|
|
read.status === "unresolved" &&
|
|
binding.mutable &&
|
|
binding.definitions.some(({ kind }) => kind === "assignment")
|
|
) {
|
|
const value = evaluateBinding(binding, nestedContext(context));
|
|
if (
|
|
value.status === "unknown" ||
|
|
value.status === "ambiguous" ||
|
|
value.status === "cycle"
|
|
)
|
|
return value;
|
|
return {
|
|
status: "unknown",
|
|
reason: `Binding ${binding.name} has unresolved ordering at this read.`,
|
|
};
|
|
}
|
|
const atRead = read.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);
|
|
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))
|
|
return mergeValues(
|
|
semanticSelectedBranches(node, context).map((branch) =>
|
|
evaluateExpression(branch, nestedContext(context)),
|
|
),
|
|
);
|
|
if (t.isObjectExpression(node)) return evaluateObject(node, context);
|
|
if (t.isArrayExpression(node)) return evaluateArray(node, context);
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
|
return evaluateMember(node, context);
|
|
if (t.isBinaryExpression(node, { operator: "+" }))
|
|
return evaluateAddition(node, context);
|
|
if (t.isUnaryExpression(node)) return evaluateUnary(node, context);
|
|
return { status: "unknown", reason: `Unsupported ${node.type} value.` };
|
|
};
|
|
|
|
const evaluateTemplate = (
|
|
node: t.TemplateLiteral,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
let candidates = [""];
|
|
for (let index = 0; index < node.quasis.length; index += 1) {
|
|
const quasi = node.quasis[index];
|
|
const text = quasi?.value.cooked;
|
|
if (text === undefined || text === null)
|
|
return {
|
|
status: "unknown",
|
|
reason: "Template text has no cooked representation.",
|
|
};
|
|
if (exceedsSemanticTemplateByteBudget(candidates, [text]))
|
|
return semanticResourceLimitUnknown("primitive-bytes");
|
|
candidates = candidates.map((prefix) => `${prefix}${text}`);
|
|
const expression = node.expressions[index];
|
|
if (expression === undefined) continue;
|
|
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");
|
|
const suffixes = values.map((value) => String(value));
|
|
if (exceedsSemanticTemplateByteBudget(candidates, suffixes))
|
|
return semanticResourceLimitUnknown("primitive-bytes");
|
|
candidates = candidates.flatMap((prefix) =>
|
|
suffixes.map((suffix) => `${prefix}${suffix}`),
|
|
);
|
|
}
|
|
return primitiveSet(candidates);
|
|
};
|
|
|
|
/** Value and provenance selection consume the same established branch facts. */
|
|
const semanticSelectedBranches = (
|
|
node: t.ConditionalExpression | t.LogicalExpression,
|
|
context: EvaluationContext,
|
|
): readonly t.Node[] => {
|
|
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];
|
|
}
|
|
const left = evaluateExpression(node.left, nestedContext(context));
|
|
const choice = logicalRightOperand(
|
|
node.operator,
|
|
semanticCondition(left, Boolean),
|
|
semanticCondition(left, (value) => value === null, false),
|
|
);
|
|
return choice === undefined
|
|
? [node.left, node.right]
|
|
: [choice ? node.right : node.left];
|
|
};
|
|
|
|
/** All retained alternatives must agree before discarding a branch. */
|
|
const semanticCondition = (
|
|
value: JavaScriptSemanticValue,
|
|
select: (value: string | number | boolean | null) => boolean,
|
|
objectResult = true,
|
|
): boolean | undefined => {
|
|
if (value.status === "object" || value.status === "array")
|
|
return objectResult;
|
|
const choices = primitiveCandidates(value)?.map(select);
|
|
return choices !== undefined &&
|
|
choices.length > 0 &&
|
|
choices.every((choice) => choice === choices[0])
|
|
? choices[0]
|
|
: undefined;
|
|
};
|
|
|
|
const evaluateObject = (
|
|
node: t.ObjectExpression,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
const propertiesByName = new Map<string, JavaScriptSemanticProperty>();
|
|
let unknownProperties = false;
|
|
let omittedProperties: number | null = 0;
|
|
const invalidateEarlierProperties = (): void => {
|
|
for (const [name] of propertiesByName)
|
|
propertiesByName.set(name, {
|
|
name,
|
|
presence: "present",
|
|
value: {
|
|
status: "unknown",
|
|
reason: "A later property may overwrite this value.",
|
|
},
|
|
});
|
|
};
|
|
for (const property of node.properties) {
|
|
if (t.isSpreadElement(property)) {
|
|
const copied = evaluateExpression(
|
|
property.argument,
|
|
captureContextAt(property.argument, nestedContext(context)) ??
|
|
nestedContext(context),
|
|
);
|
|
if (copied.status === "object" || copied.status === "array") {
|
|
const unknown =
|
|
copied.status === "object"
|
|
? copied.unknownProperties
|
|
: copied.unknownItems;
|
|
if (unknown) {
|
|
unknownProperties = true;
|
|
omittedProperties = null;
|
|
invalidateEarlierProperties();
|
|
}
|
|
for (const field of semanticContainer(copied).slots) {
|
|
if (field.presence === "absent") continue;
|
|
propertiesByName.set(field.name, {
|
|
...field,
|
|
value: isPrimitive(field.value)
|
|
? field.value
|
|
: {
|
|
status: "unknown",
|
|
reason:
|
|
"Copied reference fields retain live identity rather than a scalar snapshot.",
|
|
},
|
|
});
|
|
}
|
|
} else if (
|
|
copied.status !== "literal" ||
|
|
typeof copied.value === "string"
|
|
) {
|
|
unknownProperties = true;
|
|
omittedProperties = null;
|
|
invalidateEarlierProperties();
|
|
}
|
|
continue;
|
|
}
|
|
const name = semanticStaticPropertyKey(property.key, property.computed);
|
|
if (name === null) {
|
|
unknownProperties = true;
|
|
if (omittedProperties !== null) omittedProperties += 1;
|
|
invalidateEarlierProperties();
|
|
continue;
|
|
}
|
|
// This spelling changes the prototype instead of defining an own slot.
|
|
if (
|
|
t.isObjectProperty(property) &&
|
|
!property.computed &&
|
|
!property.shorthand &&
|
|
name === "__proto__"
|
|
) {
|
|
unknownProperties = true;
|
|
if (omittedProperties !== null) omittedProperties += 1;
|
|
continue;
|
|
}
|
|
propertiesByName.set(name, {
|
|
name,
|
|
presence: "present",
|
|
value: t.isObjectProperty(property)
|
|
? evaluateCapturedExpression(property.value, nestedContext(context))
|
|
: property.kind === "method"
|
|
? definedNonPrimitiveValue(
|
|
"Object method or accessor value is not a primitive.",
|
|
)
|
|
: {
|
|
status: "unknown",
|
|
reason: "Object method or accessor value is not a primitive.",
|
|
},
|
|
});
|
|
}
|
|
const properties = [...propertiesByName.values()].sort((left, right) =>
|
|
compareUnicodeCodePoints(left.name, right.name),
|
|
);
|
|
return unknownProperties
|
|
? {
|
|
status: "object",
|
|
properties,
|
|
unknownProperties: true,
|
|
omittedProperties,
|
|
}
|
|
: {
|
|
status: "object",
|
|
properties,
|
|
unknownProperties: false,
|
|
omittedProperties: 0,
|
|
};
|
|
};
|
|
|
|
const evaluateArray = (
|
|
node: t.ArrayExpression,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
const items: JavaScriptSemanticProperty[] = [];
|
|
let unknownItems = false;
|
|
let omittedItems: number | null = 0;
|
|
for (const element of node.elements) {
|
|
if (t.isSpreadElement(element)) {
|
|
unknownItems = true;
|
|
omittedItems = null;
|
|
// Subsequent elements have no fixed index after an unknown-length spread.
|
|
break;
|
|
}
|
|
if (element === null) {
|
|
items.push({
|
|
name: String(items.length),
|
|
presence: "absent",
|
|
value: { status: "unknown", reason: "Array hole has no own value." },
|
|
});
|
|
continue;
|
|
}
|
|
items.push({
|
|
name: String(items.length),
|
|
presence: "present",
|
|
value: evaluateCapturedExpression(element, nestedContext(context)),
|
|
});
|
|
}
|
|
return unknownItems
|
|
? { status: "array", items, unknownItems: true, omittedItems }
|
|
: {
|
|
status: "array",
|
|
items,
|
|
unknownItems: false,
|
|
omittedItems: 0,
|
|
};
|
|
};
|
|
|
|
const evaluateMember = (
|
|
node: t.MemberExpression | t.OptionalMemberExpression,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
const keys: string[] = [];
|
|
let current: t.Node = node;
|
|
let value: JavaScriptSemanticValue | undefined;
|
|
while (
|
|
t.isMemberExpression(current) ||
|
|
t.isOptionalMemberExpression(current)
|
|
) {
|
|
if (!t.isNode(current.object)) {
|
|
value = { status: "unknown", reason: "Unsupported member base." };
|
|
break;
|
|
}
|
|
const key = semanticStaticPropertyKey(current.property, current.computed);
|
|
if (key === null) {
|
|
value = { status: "unknown", reason: "Dynamic member key." };
|
|
break;
|
|
}
|
|
keys.push(key);
|
|
current = current.object;
|
|
}
|
|
value ??= evaluateExpression(current, nestedContext(context));
|
|
for (const key of keys.reverse()) value = projectValue(value, [key]);
|
|
return value;
|
|
};
|
|
|
|
const evaluateAddition = (
|
|
node: t.BinaryExpression,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
const pending: {
|
|
readonly node: t.BinaryExpression;
|
|
left: JavaScriptSemanticValue | undefined;
|
|
}[] = [];
|
|
let current: t.Node = node;
|
|
while (true) {
|
|
if (t.isBinaryExpression(current, { operator: "+" })) {
|
|
pending.push({ node: current, left: undefined });
|
|
current = current.left;
|
|
continue;
|
|
}
|
|
let value = evaluateExpression(current, nestedContext(context));
|
|
while (true) {
|
|
const parent = pending.at(-1);
|
|
if (parent === undefined) return value;
|
|
if (parent.left === undefined) {
|
|
parent.left = value;
|
|
current = parent.node.right;
|
|
break;
|
|
}
|
|
value = addPrimitiveValues(parent.left, value);
|
|
pending.pop();
|
|
}
|
|
}
|
|
};
|
|
|
|
const addPrimitiveValues = (
|
|
leftValue: JavaScriptSemanticValue,
|
|
rightValue: JavaScriptSemanticValue,
|
|
): JavaScriptSemanticValue => {
|
|
const left = primitiveCandidates(leftValue);
|
|
const right = primitiveCandidates(rightValue);
|
|
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 > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT / right.length)
|
|
return semanticResourceLimitUnknown("primitive-candidates");
|
|
if (exceedsSemanticPrimitiveAdditionByteBudget(left, right))
|
|
return semanticResourceLimitUnknown("primitive-bytes");
|
|
const values = left.flatMap((leftValue) =>
|
|
right.map((rightValue) =>
|
|
typeof leftValue === "string" || typeof rightValue === "string"
|
|
? `${String(leftValue)}${String(rightValue)}`
|
|
: Number(leftValue) + Number(rightValue),
|
|
),
|
|
);
|
|
return primitiveSet(values);
|
|
};
|
|
|
|
const evaluateUnary = (
|
|
node: t.UnaryExpression,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
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 === "+")
|
|
return primitiveSet(argument.map((value) => Number(value)));
|
|
if (node.operator === "-")
|
|
return primitiveSet(argument.map((value) => -Number(value)));
|
|
return { status: "unknown", reason: `Unsupported unary ${node.operator}.` };
|
|
};
|
|
|
|
const projectValue = (
|
|
value: JavaScriptSemanticValue,
|
|
projection: readonly (string | number | null)[],
|
|
): JavaScriptSemanticValue => {
|
|
if (isSemanticResourceLimit(value)) return value;
|
|
if (projection.includes(null))
|
|
return { status: "unknown", reason: "Cannot project a dynamic property." };
|
|
const path = projection.map((key) => String(key));
|
|
const slot = semanticSlotAtPath(value, path);
|
|
return slot.presence === "present" || isSemanticResourceLimit(slot.value)
|
|
? slot.value
|
|
: {
|
|
status: "unknown",
|
|
reason: `Own property ${path.join("/")} is ${slot.presence}.`,
|
|
};
|
|
};
|
|
|
|
const provenanceForBinding = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): JavaScriptBindingProvenance => {
|
|
recordBindingDependency(binding, context);
|
|
const cycleHead = context.bindings.get(binding.bindingId);
|
|
if (cycleHead !== undefined) {
|
|
context.dependence.lowest = Math.min(context.dependence.lowest, cycleHead);
|
|
return semanticUnresolvedProvenance(
|
|
"cycle",
|
|
`Alias cycle at ${binding.name}.`,
|
|
);
|
|
}
|
|
let depths = context.memo.provenance.get(binding.bindingId);
|
|
const prior = readMemoizedEvaluation(
|
|
depths?.get(context.expressionDepth),
|
|
context,
|
|
);
|
|
if (prior !== undefined) return prior;
|
|
const position = context.bindings.size;
|
|
const callerDependence = context.dependence.lowest;
|
|
context.dependence.lowest = CALLER_INDEPENDENT;
|
|
let result: MemoizedEvaluation<JavaScriptBindingProvenance>;
|
|
let callerIndependent = false;
|
|
try {
|
|
result = collectEvaluationDependencies(binding, context, () =>
|
|
provenanceForBindingUncached(binding, context),
|
|
);
|
|
callerIndependent = context.dependence.lowest > position;
|
|
} finally {
|
|
context.dependence.lowest = callerIndependent
|
|
? callerDependence
|
|
: Math.min(callerDependence, context.dependence.lowest);
|
|
}
|
|
if (callerIndependent) {
|
|
if (depths === undefined) {
|
|
depths = new Map();
|
|
context.memo.provenance.set(binding.bindingId, depths);
|
|
}
|
|
depths.set(context.expressionDepth, result);
|
|
}
|
|
return result.value;
|
|
};
|
|
|
|
const provenanceForBindingUncached = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): JavaScriptBindingProvenance => {
|
|
const provenance = provenanceForBindingValue(binding, context);
|
|
const assigned = binding.definitions.some(
|
|
({ kind }) => kind === "assignment",
|
|
);
|
|
const conditionallyInitialized = binding.initializers.some(({ node }) =>
|
|
context.state.conditionallySkippedExpressions.has(node),
|
|
);
|
|
if (
|
|
provenance.status !== "module" ||
|
|
(!assigned && !conditionallyInitialized)
|
|
)
|
|
return provenance;
|
|
return semanticAmbiguousProvenance(
|
|
provenance.origins,
|
|
assigned
|
|
? `Binding ${binding.name} may be reassigned.`
|
|
: `Binding ${binding.name} may remain uninitialized.`,
|
|
);
|
|
};
|
|
|
|
const provenanceForBindingValue = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): JavaScriptBindingProvenance => {
|
|
const provenance = provenanceForBindingInitializers(binding, context);
|
|
if (!binding.referenceInitializers.some(({ assignment }) => assignment))
|
|
return provenance;
|
|
// Projected replacements are retained reference sources, not evidence of a
|
|
// local value or an unchanged earlier module. Apply this only to the selected
|
|
// read snapshot so a later replacement does not erase captured origins.
|
|
if (provenance.status === "local")
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
`Binding ${binding.name} has projected reference assignments whose module provenance is unresolved.`,
|
|
);
|
|
return provenance.status === "module"
|
|
? semanticAmbiguousProvenance(
|
|
provenance.origins,
|
|
`Binding ${binding.name} has projected reference assignments that may replace its module origin.`,
|
|
)
|
|
: provenance;
|
|
};
|
|
|
|
const provenanceForBindingInitializers = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
context: EvaluationContext,
|
|
): JavaScriptBindingProvenance => {
|
|
if (binding.kind === "parameter" || binding.kind === "catch")
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
binding.kind === "parameter"
|
|
? "Parameter provenance depends on caller arguments."
|
|
: "Catch binding provenance depends on the thrown value.",
|
|
);
|
|
if (binding.directOrigins.length > 0)
|
|
return binding.kind === "import" &&
|
|
!binding.runtimeImport &&
|
|
binding.directOrigins.every(
|
|
({ resolutionMode }) => resolutionMode !== "require",
|
|
)
|
|
? semanticAmbiguousProvenance(
|
|
binding.directOrigins,
|
|
"A type-only import does not establish a runtime module value.",
|
|
)
|
|
: semanticOriginsProvenance(binding.directOrigins);
|
|
if (binding.initializers.length === 0) return semanticLocalProvenance();
|
|
if (binding.initializers.length > 1)
|
|
return semanticAmbiguousProvenance(
|
|
[],
|
|
`Binding ${binding.name} has multiple possible assignments.`,
|
|
);
|
|
const initializer = binding.initializers[0];
|
|
if (initializer === undefined)
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
"Missing binding initializer.",
|
|
);
|
|
if (initializer.projection.includes(null))
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
"Dynamic provenance projection.",
|
|
);
|
|
context.bindings.set(binding.bindingId, context.bindings.size);
|
|
let resolved: JavaScriptBindingProvenance;
|
|
try {
|
|
resolved = provenanceForExpression(
|
|
initializer.node,
|
|
nestedContext(context),
|
|
);
|
|
} finally {
|
|
context.bindings.delete(binding.bindingId);
|
|
}
|
|
if (resolved.status !== "module" || initializer.projection.length === 0)
|
|
return resolved;
|
|
return semanticOriginsProvenance(
|
|
resolved.origins.map((origin) => ({
|
|
...origin,
|
|
importedPath: [
|
|
...origin.importedPath,
|
|
...initializer.projection.map((segment) => String(segment)),
|
|
],
|
|
})),
|
|
);
|
|
};
|
|
|
|
const provenanceForExpression = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): JavaScriptBindingProvenance => {
|
|
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
|
context.dependence.lowest = EVERY_CALLER;
|
|
context.state.valueResourceLimits.add("expression-depth");
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
semanticResourceLimitReason("expression-depth"),
|
|
);
|
|
}
|
|
const unwrapped = unwrapJavaScriptExpression(node);
|
|
if (unwrapped.depth > 0) {
|
|
if (
|
|
context.expressionDepth + unwrapped.depth >
|
|
SEMANTIC_EXPRESSION_DEPTH_LIMIT
|
|
) {
|
|
context.dependence.lowest = EVERY_CALLER;
|
|
context.state.valueResourceLimits.add("expression-depth");
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
semanticResourceLimitReason("expression-depth"),
|
|
);
|
|
}
|
|
return provenanceForExpression(unwrapped.node, {
|
|
...context,
|
|
expressionDepth: context.expressionDepth + unwrapped.depth,
|
|
});
|
|
}
|
|
const required = semanticRequireOrigin(node, context.state);
|
|
if (required !== undefined)
|
|
return semanticOriginsProvenance([required.origin]);
|
|
if (t.isSequenceExpression(node)) {
|
|
const finalExpression = node.expressions.at(-1);
|
|
return finalExpression === undefined
|
|
? semanticUnresolvedProvenance("unknown", "Empty sequence expression.")
|
|
: provenanceForExpression(finalExpression, nestedContext(context));
|
|
}
|
|
if (t.isAssignmentExpression(node) && node.operator === "=")
|
|
return provenanceForExpression(node.right, nestedContext(context));
|
|
if (t.isIdentifier(node)) {
|
|
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
|
return binding === undefined
|
|
? semanticUnresolvedProvenance(
|
|
"unknown",
|
|
`Unbound identifier ${node.name}.`,
|
|
)
|
|
: provenanceForBindingAtRead(binding, node, nestedContext(context));
|
|
}
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
const members: string[] = [];
|
|
let current: t.Node = node;
|
|
while (
|
|
t.isMemberExpression(current) ||
|
|
t.isOptionalMemberExpression(current)
|
|
) {
|
|
if (!t.isNode(current.object))
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
"Unsupported member base.",
|
|
);
|
|
const member = semanticStaticPropertyKey(
|
|
current.property,
|
|
current.computed,
|
|
);
|
|
if (member === null)
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
"Dynamic provenance member.",
|
|
);
|
|
members.push(member);
|
|
current = current.object;
|
|
}
|
|
const base = provenanceForExpression(current, nestedContext(context));
|
|
const path = members.reverse();
|
|
return base.status !== "module"
|
|
? base
|
|
: semanticOriginsProvenance(
|
|
base.origins.map((origin) => ({
|
|
...origin,
|
|
importedPath: [...origin.importedPath, ...path],
|
|
})),
|
|
);
|
|
}
|
|
if (t.isConditionalExpression(node) || t.isLogicalExpression(node)) {
|
|
const candidates = semanticSelectedBranches(node, context).map((branch) =>
|
|
provenanceForExpression(branch, nestedContext(context)),
|
|
);
|
|
const origins = uniqueSemanticOrigins(
|
|
candidates.flatMap((candidate) => candidate.origins),
|
|
);
|
|
const allModuleOrigins = candidates.every(
|
|
({ status }) => status === "module",
|
|
);
|
|
if (allModuleOrigins && origins.length === 1)
|
|
return semanticOriginsProvenance(origins);
|
|
return origins.length > 0
|
|
? semanticAmbiguousProvenance(
|
|
origins,
|
|
allModuleOrigins
|
|
? "Multiple module origins."
|
|
: "Conditional provenance includes a non-module alternative.",
|
|
)
|
|
: semanticUnresolvedProvenance(
|
|
"unknown",
|
|
"Conditional provenance is unresolved.",
|
|
);
|
|
}
|
|
if (isLocallyCreatedValue(node)) return semanticLocalProvenance();
|
|
return semanticUnresolvedProvenance(
|
|
"unknown",
|
|
`Unsupported ${node.type} provenance.`,
|
|
);
|
|
};
|
|
|
|
/** Resolve an alias from the value visible at this read, before later rebinds. */
|
|
const provenanceForBindingAtRead = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
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,
|
|
context.state.parentsByNode,
|
|
);
|
|
if (snapshot === binding) return provenanceForBinding(binding, context);
|
|
if (context.bindings.has(binding.bindingId))
|
|
return semanticUnresolvedProvenance(
|
|
"cycle",
|
|
`Alias cycle at ${binding.name}.`,
|
|
);
|
|
context.bindings.set(binding.bindingId, context.bindings.size);
|
|
try {
|
|
return provenanceForBindingValue(snapshot, context);
|
|
} finally {
|
|
context.bindings.delete(binding.bindingId);
|
|
}
|
|
};
|
|
|
|
const isLocallyCreatedValue = (node: t.Node): boolean =>
|
|
readExactJavaScriptLiteral(node).found ||
|
|
t.isObjectExpression(node) ||
|
|
t.isArrayExpression(node) ||
|
|
t.isFunctionExpression(node) ||
|
|
t.isFunctionDeclaration(node) ||
|
|
t.isArrowFunctionExpression(node) ||
|
|
t.isClassExpression(node) ||
|
|
t.isClassDeclaration(node) ||
|
|
t.isTemplateLiteral(node) ||
|
|
t.isUnaryExpression(node) ||
|
|
t.isBinaryExpression(node) ||
|
|
t.isUpdateExpression(node);
|
|
|
|
const mergeValues = (
|
|
values: readonly JavaScriptSemanticValue[],
|
|
): JavaScriptSemanticValue => {
|
|
const resourceLimit = values.find(isSemanticResourceLimit);
|
|
if (resourceLimit !== undefined) return resourceLimit;
|
|
const primitives = values.flatMap(
|
|
(value) => primitiveCandidates(value) ?? [],
|
|
);
|
|
const merged: JavaScriptSemanticValue = values.every(
|
|
(value) => value.status === "union" || value.status === "literal",
|
|
)
|
|
? primitiveSet(primitives)
|
|
: { status: "ambiguous", reason: "Branches have incompatible values." };
|
|
if (values.length > 0 && values.every(semanticValueDefinitelyDefined))
|
|
definitelyDefinedValues.add(merged);
|
|
return merged;
|
|
};
|
|
|
|
const nestedContext = (context: EvaluationContext): EvaluationContext => ({
|
|
state: context.state,
|
|
primitiveBindingValues: context.primitiveBindingValues,
|
|
...(context.capturePoint === undefined
|
|
? {}
|
|
: { capturePoint: context.capturePoint }),
|
|
expressionDepth: context.expressionDepth + 1,
|
|
bindings: context.bindings,
|
|
dependence: context.dependence,
|
|
memo: context.memo,
|
|
});
|
|
|
|
const isPrimitive = (value: JavaScriptSemanticValue): boolean =>
|
|
value.status === "literal" || value.status === "union";
|
|
|
|
/** Capturing cannot restore an exhausted expression-depth budget. */
|
|
const canCapturePrimitive = (value: JavaScriptSemanticValue): boolean =>
|
|
!isPrimitive(value) &&
|
|
!(
|
|
isSemanticResourceLimit(value) && value.resourceLimit === "expression-depth"
|
|
);
|
|
|
|
const primitiveCaptureContext = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): EvaluationContext | undefined => {
|
|
if (t.isObjectExpression(node) || t.isArrayExpression(node)) return undefined;
|
|
return captureContextAt(node, context);
|
|
};
|
|
|
|
const captureContextAt = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): EvaluationContext | undefined => {
|
|
if (
|
|
context.capturePoint !== undefined ||
|
|
semanticCapturePosition(node, context.state.parentsByNode) === null
|
|
)
|
|
return undefined;
|
|
let captures = capturedBindingValuesByState.get(context.state);
|
|
if (captures === undefined) {
|
|
captures = new WeakMap();
|
|
capturedBindingValuesByState.set(context.state, captures);
|
|
}
|
|
let values = captures.get(node);
|
|
if (values === undefined) {
|
|
values = new Map();
|
|
captures.set(node, values);
|
|
}
|
|
return { ...context, capturePoint: node, primitiveBindingValues: values };
|
|
};
|
|
|
|
const evaluateCapturedExpression = (
|
|
node: t.Node,
|
|
context: EvaluationContext,
|
|
): JavaScriptSemanticValue => {
|
|
// A literal field captures its primitive at construction, even when its
|
|
// containing object is subsequently read at another capture point.
|
|
let values = primitiveExpressionValuesByState.get(context.state);
|
|
const cached = values?.get(node);
|
|
if (cached !== undefined) return cached;
|
|
let value = evaluateExpression(node, context);
|
|
if (canCapturePrimitive(value)) {
|
|
const captureContext = primitiveCaptureContext(node, context);
|
|
if (captureContext !== undefined) {
|
|
const captured = evaluateExpression(node, captureContext);
|
|
if (isPrimitive(captured)) value = captured;
|
|
}
|
|
}
|
|
// Never publish values evaluated at somebody else's capture point. Like
|
|
// binding caches, these entries are discarded whenever effects change.
|
|
if (context.capturePoint === undefined && isPrimitive(value)) {
|
|
if (values === undefined) {
|
|
values = new WeakMap();
|
|
primitiveExpressionValuesByState.set(context.state, values);
|
|
}
|
|
values.set(node, value);
|
|
}
|
|
return value;
|
|
};
|