( expr: Expression, forms: CanonicalOptions, scope?: Scope )
| 12 | import { canonicalMultiply, canonicalDivide } from './arithmetic-mul-div.js'; |
| 13 | import { canonicalPower } from './arithmetic-power.js'; |
| 14 | import { canonicalOrder } from './order.js'; |
| 15 | import { declaredBinders, binderShadowAt } from './binding-sites.js'; |
| 16 | import { asBigint } from './numerics.js'; |
| 17 | import { isOperatorDef, isImaginaryUnit } from './utils.js'; |
| 18 | import { isFunction, isNumber, isSymbol } from './type-guards.js'; |
| 19 | |
| 20 | export function canonicalForm( |
| 21 | expr: Expression, |
| 22 | forms: CanonicalOptions, |
| 23 | scope?: Scope |
| 24 | ): Expression { |
| 25 | // No canonical form? |
| 26 | if (forms === false) return expr; |
| 27 | |
| 28 | // Full canonical form? Without a scope, `_inScope()` would just call its |
| 29 | // callback: go straight to the getter. This is on the recursive path of |
| 30 | // canonicalizing an already-boxed tree (each operand's `.canonical` comes |
| 31 | // back through here), where every frame per level costs depth. |
| 32 | if (forms === true) |
| 33 | return scope === undefined |
| 34 | ? expr.canonical |
| 35 | : expr.engine._inScope(scope, () => expr.canonical); |
| 36 | |
| 37 | if (typeof forms === 'string') forms = [forms]; |
| 38 | |
| 39 | // A partial form is not fully canonical, so it follows the STRUCTURAL symbol |
| 40 | // contract: symbols resolve against the scope chain (a `holdUntil: 'never'` |
| 41 | // constant still substitutes its value, an existing declaration still binds), |
| 42 | // but a name that resolves to nothing is left unbound instead of being |
| 43 | // declared into the caller's scope. The suppression wraps the whole pipeline |
| 44 | // because the forms reach `.canonical` on a symbol through many helpers |
| 45 | // (`isImaginaryUnit`, `flatten`, `canonicalInvisibleOperator`, …), not just |
| 46 | // `symbolForm`. See `docs/SCOPING-MODEL.md` A1. |
| 47 | // |
| 48 | // The supplied scope is honored on this path too (B1): it steers the lookups |
| 49 | // the forms perform, exactly as it does on the `forms === true` path above. |
| 50 | // Without it, `scope` was silently ignored for every partial form. |
| 51 | const formList = forms; |
| 52 | return expr.engine._inScope(scope, () => |
| 53 | expr.engine._resolveOnly(() => applyForms(expr, formList)) |
| 54 | ); |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * `expr`'s symbol name, or `''` for anything that is not a symbol — a sentinel |
| 59 | * no binder can bind, so a shadow-set membership test on it is always false. |
| 60 | */ |
| 61 | function symbolNameOf(expr: Expression): string { |
| 62 | return isSymbol(expr) ? expr.symbol : ''; |
| 63 | } |
| 64 | |
| 65 | function applyForms( |
| 66 | expr: Expression, |
| 67 | forms: readonly CanonicalForm[] |
| 68 | ): Expression { |
| 69 | // Like for full canonicalization, request the canonical form of symbols. |
| 70 | // Automatically, this involves the substitution of the symbol with its |
| 71 | // value, if it is a constant-flagged symbol, with a 'holdUntil' attribute of |
no test coverage detected