(
ce: IComputeEngine,
options?: { details?: boolean; maxDepth?: number }
)
| 80 | * A no-op if the context is not on the stack (already removed). |
| 81 | */ |
| 82 | export function removeEvalContext( |
| 83 | ce: IComputeEngine, |
| 84 | context: EvalContext |
| 85 | ): void { |
| 86 | const index = ce._evalContextStack.lastIndexOf(context); |
| 87 | if (index < 0) return; |
| 88 | ce._evalContextStack.splice(index, 1); |
| 89 | discardEvalContext(ce, context); |
| 90 | } |
| 91 | |
| 92 | function discardEvalContext( |
| 93 | ce: IComputeEngine, |
| 94 | context: EvalContext | undefined |
| 95 | ): void { |
| 96 | // A checkpoint standing on this frame has no world left to restore once |
| 97 | // the frame's bindings are disposed below — retire it, folding its journal |
| 98 | // window downward so older checkpoints still unwind this scope's writes |
| 99 | // (`checkpoint.ts`). Gated on the stack so the no-checkpoint path pays one |
| 100 | // length read. |
| 101 | if (context !== undefined && ce._checkpointStack.length > 0) |
| 102 | ce._invalidateCheckpointsOnFrameDiscard(context); |
| 103 | // Definitions owned by a scope may subscribe to engine-wide lifecycle |
| 104 | // events. Release those subscriptions as soon as the scope is discarded, |
| 105 | // rather than retaining otherwise-dead local constants for the lifetime of |
| 106 | // the engine. Disposal is intentionally idempotent. |
| 107 | for (const binding of context?.lexicalScope.bindings.values() ?? []) { |
| 108 | if (isValueDef(binding)) { |
| 109 | // Debug invariant (§3 of the binder-mechanism design): stamp BEFORE |
| 110 | // disposing, so a later use of this binding reports where its scope died. |
| 111 | // A DORMANT pop is exempt: `canonicalizeBinder` pops a scope the |
| 112 | // canonical expression keeps and pushes again on every evaluation, so |
| 113 | // that pop is not the scope's death and a tombstone there would report |
| 114 | // every bound variable of every canonicalized binder. |
| 115 | if (ce._debugBindings && !isDormantPop()) |
| 116 | tombstoneBinding(binding.value, context?.name ?? '<unnamed>'); |
| 117 | binding.value.dispose(); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | // Popping an eval context reverts the active assumptions and local |
| 122 | // declarations to the enclosing context. Per-expression caches keyed on |
| 123 | // `ce._anyVersion` (e.g. `BoxedFunction.sgn`/`.type`) would otherwise keep |
| 124 | // returning values computed under the popped scope's assumptions — a stale |
| 125 | // read on any expression held across the scope. `assume()`/`forget()` |
| 126 | // advance the axis on the way in, but the revert on the way out is silent, |
| 127 | // so this event covers it. (A matching event on push is not needed: |
| 128 | // `pushEvalContext` copies the current assumptions unchanged, and any |
| 129 | // assumption added inside the scope goes through `assume()`.) The |
| 130 | // `assumptionsDirty` payload carries the M+E half: `_semanticVersion` (the |
| 131 | // key of the `Comprehension` element memo) advances ONLY when this |
| 132 | // context's assumptions were modified — a clean pop leaves it untouched so |
| 133 | // mutation-keyed caches survive unrelated scoped evaluations (Tycho |
| 134 | // item 38). |
| 135 | // |
| 136 | // The `clean` payload carries the `any` half of the same argument: when |
| 137 | // NOTHING advanced any of the three axes while this context was on the |
| 138 | // stack (every install of a local binding or value goes through an event |
| 139 | // that advances at least one — `declare` and `value-write` including |
nothing calls this directly
no test coverage detected