(def: unknown)
| 359 | * `Solve(s = 2, w)` with `s := (9 - w²)/4` saw an equation with no `w` in it |
| 360 | * and returned `[]` — which by contract means "proven no solutions". A |
| 361 | * coefficient symbol was already resolved further down the pipeline; only a |
| 362 | * binding that conceals the unknown was mishandled. |
| 363 | * |
| 364 | * Reads the *stored* value (`.value`), never `.evaluate()`: evaluating would |
| 365 | * resolve the unknown inside that value too (with `w := 7`, `s.evaluate()` |
| 366 | * would fold `w` away). `protect` holds the unknowns, so the variable being |
| 367 | * solved for is never substituted, and `seen` stops a self-referential or |
| 368 | * mutually-referential binding from looping. |
| 369 | */ |
| 370 | export function resolveBoundSymbols( |
| 371 | expr: Expression, |
| 372 | protect: ReadonlySet<string>, |
| 373 | seen: Set<string> = new Set() |
| 374 | ): Expression { |
| 375 | if (isSymbol(expr)) { |
| 376 | const name = expr.symbol; |
| 377 | if (protect.has(name) || seen.has(name)) return expr; |
| 378 | const def = expr.engine.lookupDefinition(name); |
| 379 | if (!isValueDef(def)) return expr; |
| 380 | const value = def.value.value; |
| 381 | if (value === undefined || value === null) return expr; |
| 382 | seen.add(name); |
| 383 | const resolved = resolveBoundSymbols(value, protect, seen); |
| 384 | seen.delete(name); |
| 385 | return resolved; |
| 386 | } |
| 387 | |
| 388 | if (!isFunction(expr)) return expr; |
| 389 | |
| 390 | // Binder-awareness: a `Function` literal, `Block`, `Sum`, etc. binds its own |
| 391 | // variables. Those must NOT be resolved to a same-named GLOBAL value — |
| 392 | // `Simplify(x ↦ x + 1)` with `x := 5` must stay `x ↦ x + 1`, not corrupt the |
| 393 | // body's bound `x` into `5`. Extend the protected set with the locally-bound |
| 394 | // names before descending. (`localScope` covers `Block`/`Sum`/`Product`/…; |
| 395 | // a `Function`'s parameters live in its operand slots, not its scope.) |
| 396 | const bound = boundVariableNames(expr); |
| 397 | const childProtect = bound.length ? new Set([...protect, ...bound]) : protect; |
| 398 | |
| 399 | const ops = expr.ops; |
| 400 | const resolved = ops.map((op) => resolveBoundSymbols(op, childProtect, seen)); |
| 401 | if (resolved.every((op, i) => op === ops[i])) return expr; |
| 402 | return expr.engine.function(expr.operator, resolved); |
| 403 | } |
| 404 | |
| 405 | /** |
| 406 | * Replace `At(List(e₁, …, eₙ), k)` by `e_k` — a purely *structural* |
no test coverage detected