mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(javascript): preserve values captured before later assignments (#4196)
Select binding facts at reads without mixing whole-binding and temporal values. Share conditional activation rules, preserve partial expression order, and bound immutable ancestry work. Memoize temporal results within each query using initializer identity, capture point, budget and dependency proofs; preserve depth limits and source Evidence. Fixes #3881
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
semanticCapturePosition,
|
||||
semanticEscapeFollowsCapture,
|
||||
semanticBindingPrecedesCapture,
|
||||
semanticReadInitializers,
|
||||
semanticBindingAtRead,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
|
||||
import { semanticSlotAtPath } from "./javascriptSemanticSlots.js";
|
||||
@@ -63,8 +63,19 @@ interface MemoizedEvaluation<T> {
|
||||
readonly dependencies: SemanticDependencyProof;
|
||||
}
|
||||
|
||||
/** Provenance lives for one query; live values belong to the effects-aware state. */
|
||||
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>>
|
||||
@@ -291,9 +302,50 @@ const collectEvaluationDependencies = <T>(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -302,6 +354,7 @@ const evaluateBinding = (
|
||||
return { status: "cycle", reason: `Alias cycle at ${binding.name}.` };
|
||||
}
|
||||
if (
|
||||
wholeBinding &&
|
||||
context.capturePoint !== undefined &&
|
||||
!semanticBindingPrecedesCapture(
|
||||
binding,
|
||||
@@ -315,14 +368,28 @@ const evaluateBinding = (
|
||||
primitiveBindingValues: primitiveBindingValuesFor(context.state),
|
||||
});
|
||||
}
|
||||
const cached = context.primitiveBindingValues.get(binding.bindingId);
|
||||
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 =
|
||||
context.capturePoint === undefined
|
||||
wholeBinding && context.capturePoint === undefined
|
||||
? callerIndependentBindingValuesFor(context.state)
|
||||
: undefined;
|
||||
const retained = independent?.get(binding.bindingId);
|
||||
@@ -336,7 +403,9 @@ const evaluateBinding = (
|
||||
if (binding.initializers.length === 0)
|
||||
return {
|
||||
status: "unknown",
|
||||
reason: `Binding ${binding.name} has no constant initializer.`,
|
||||
reason: wholeBinding
|
||||
? `Binding ${binding.name} has no constant initializer.`
|
||||
: `Binding ${binding.name} has no proven initializer at this read.`,
|
||||
};
|
||||
if (
|
||||
binding.initializers.some(
|
||||
@@ -372,6 +441,7 @@ const evaluateBinding = (
|
||||
context.bindings.set(binding.bindingId, position);
|
||||
let value: JavaScriptSemanticValue;
|
||||
let callerIndependent = false;
|
||||
let depthDependent = false;
|
||||
let evaluated: MemoizedEvaluation<JavaScriptSemanticValue>;
|
||||
try {
|
||||
evaluated = collectEvaluationDependencies(binding, context, () => {
|
||||
@@ -380,7 +450,7 @@ const evaluateBinding = (
|
||||
evaluateExpression(initializer.node, nested),
|
||||
initializer.projection,
|
||||
);
|
||||
if (!isPrimitive(value)) {
|
||||
if (canCapturePrimitive(value)) {
|
||||
const captureContext = primitiveCaptureContext(
|
||||
initializer.node,
|
||||
nested,
|
||||
@@ -399,6 +469,7 @@ const evaluateBinding = (
|
||||
// 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
|
||||
@@ -435,15 +506,31 @@ const evaluateBinding = (
|
||||
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 (
|
||||
context.capturePoint !== undefined ||
|
||||
projected.status === "literal" ||
|
||||
projected.status === "union" ||
|
||||
(isSemanticResourceLimit(projected) &&
|
||||
(projected.resourceLimit === "primitive-bytes" ||
|
||||
projected.resourceLimit === "primitive-candidates"))
|
||||
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;
|
||||
@@ -484,9 +571,28 @@ const evaluateExpressionValue = (
|
||||
if (literal.found) return primitiveSet([literal.value]);
|
||||
if (t.isIdentifier(node)) {
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
return binding === undefined
|
||||
? { status: "unknown", reason: `Unbound identifier ${node.name}.` }
|
||||
: evaluateBinding(binding, nestedContext(context));
|
||||
if (binding === undefined)
|
||||
return { status: "unknown", reason: `Unbound identifier ${node.name}.` };
|
||||
const read =
|
||||
binding.mutable &&
|
||||
binding.definitions.some(({ kind }) => kind === "assignment")
|
||||
? semanticBindingAtRead(binding, node, context.state.parentsByNode)
|
||||
: undefined;
|
||||
if (read?.status === "unresolved") {
|
||||
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 ?? binding;
|
||||
return evaluateBinding(atRead, nestedContext(context), atRead === binding);
|
||||
}
|
||||
if (t.isTemplateLiteral(node)) return evaluateTemplate(node, context);
|
||||
if (t.isConditionalExpression(node))
|
||||
@@ -1021,26 +1127,17 @@ const provenanceForBindingAtRead = (
|
||||
read: t.Identifier,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptBindingProvenance => {
|
||||
const selected = semanticReadInitializers(
|
||||
const { binding: snapshot } = semanticBindingAtRead(
|
||||
binding,
|
||||
read,
|
||||
context.state.parentsByNode,
|
||||
);
|
||||
if (selected.initializers === binding.initializers)
|
||||
return provenanceForBinding(binding, context);
|
||||
if (snapshot === binding) return provenanceForBinding(binding, context);
|
||||
if (context.bindings.has(binding.bindingId))
|
||||
return semanticUnresolvedProvenance(
|
||||
"cycle",
|
||||
`Alias cycle at ${binding.name}.`,
|
||||
);
|
||||
const snapshot: JavaScriptSemanticBindingState = {
|
||||
...binding,
|
||||
...selected,
|
||||
// Assignment facts after the read cannot change the captured value.
|
||||
definitions: binding.definitions.filter(
|
||||
({ kind }) => kind !== "assignment",
|
||||
),
|
||||
};
|
||||
context.bindings.set(binding.bindingId, context.bindings.size);
|
||||
try {
|
||||
return provenanceForBindingValue(snapshot, context);
|
||||
@@ -1091,6 +1188,13 @@ const nestedContext = (context: EvaluationContext): EvaluationContext => ({
|
||||
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,
|
||||
@@ -1131,7 +1235,7 @@ const evaluateCapturedExpression = (
|
||||
const cached = values?.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
let value = evaluateExpression(node, context);
|
||||
if (!isPrimitive(value)) {
|
||||
if (canCapturePrimitive(value)) {
|
||||
const captureContext = primitiveCaptureContext(node, context);
|
||||
if (captureContext !== undefined) {
|
||||
const captured = evaluateExpression(node, captureContext);
|
||||
|
||||
Reference in New Issue
Block a user