( oldExpr: Expression, newExpr: Expression | null | undefined, costFunction?: (expr: Expression) => number )
| 232 | // Mark it as a SHIELD — see `withValueShield`, the other site. |
| 233 | markShieldDeclaration(shadowScope, n); |
| 234 | } catch { |
| 235 | /* leave this symbol unshadowed */ |
| 236 | } |
| 237 | }); |
| 238 | steps = simplify(expr, options); |
| 239 | } finally { |
| 240 | ce.popScope(); |
| 241 | } |
| 242 | // The shadow bindings are dead now that the scope is popped, but a result |
| 243 | // that could not fully reduce still mentions the shielded symbols and would |
| 244 | // carry those dead bindings out to the caller — where they compare unequal |
| 245 | // to the caller's own symbols. Same fix as `withValueShield`; this is the |
| 246 | // second site of the same pattern, reached by the public `.simplify()`. |
| 247 | // |
| 248 | // Only the FINAL step's value escapes as the caller's result; the |
| 249 | // intermediate steps exist for `explain()`, which serializes them by name. |
| 250 | // Rebinding only the last value keeps the fix off the O(steps × tree) path. |
| 251 | const last = steps.at(-1); |
| 252 | if (last === undefined) return steps; |
| 253 | const rebound = rebindEscaping(last.value, shadowScope); |
| 254 | if (rebound === last.value) return steps; |
| 255 | return [...steps.slice(0, -1), { ...last, value: rebound }] as RuleSteps; |
| 256 | } |
| 257 | |
| 258 | export function simplify( |
| 259 | expr: Expression, |
no test coverage detected