( expr: Expression )
| 279 | // opaque) — value-safe, and strictly better than silently corrupting. |
| 280 | const binders = collectBinderNames(body); |
| 281 | if (binders.size > 0) { |
| 282 | for (const name of Object.keys(substitution)) { |
| 283 | if (binders.has(name)) return undefined; |
| 284 | for (const s of substitution[name].symbols) |
| 285 | if (binders.has(s)) return undefined; |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | return body.subs(substitution); |
| 290 | } |
| 291 | |
| 292 | /** Every name bound by a binder anywhere within `expr` (its own bound names |
| 293 | * plus those of every descendant), used to keep lambda inlining capture-safe. */ |
| 294 | export function collectBinderNames( |
| 295 | expr: Expression, |
| 296 | acc: Set<string> = new Set() |
| 297 | ): Set<string> { |
| 298 | if (!isFunction(expr)) return acc; |
| 299 | for (const n of boundVariableNames(expr)) acc.add(n); |
| 300 | for (const op of expr.ops) collectBinderNames(op, acc); |
| 301 | return acc; |
| 302 | } |
| 303 | |
| 304 | /** |
| 305 | * Inline applications of user-defined functions throughout `expr`. |
| 306 | * |
| 307 | * A lazy operator holds its expression operand and takes only `.canonical`, |
| 308 | * which binds structure without substituting values. A call to a user-defined |
| 309 | * function therefore arrived as an opaque node that the algorithm could not |
| 310 | * see into: `Simplify(g(a))` returned `g(a)`, `Integrate(g(t), t)` stayed |
| 311 | * inert, and — worst — `Solve(g(x) = 0, x)` returned `[]`, which by contract |
| 312 | * means "proven no solutions". |
| 313 | * |
| 314 | * `budget` bounds the TOTAL number of beta-reductions so a self-recursive |
| 315 | * definition (`fact(n) = … fact(n - 1) …`) cannot loop forever, while a finite |
| 316 | * self-composition (`g(g(x))` for a non-recursive `g`) still fully expands — an |
| 317 | * on-path name guard would wrongly stop the inner `g(x)`, leaving `Solve` an |
| 318 | * opaque `g(x)` it reads as "no solutions". `budget` is a single object shared |
| 319 | * by reference across every branch of the traversal, so it is one global cap on |
| 320 | * the TOTAL number of beta-reductions in the whole tree — sibling calls |
| 321 | * (`g(a) + g(b)`) draw down the same counter rather than each getting a fresh |
| 322 | * budget. That shared cap is what bounds a self-recursive definition. |
| 323 | */ |
| 324 | // Generous enough that no realistic expression (a wide system of many function |
no test coverage detected