( a: Expression, inputB: number | Expression )
| 134 | // The lenient form — either side unbound ⇒ equal — was not merely "outside |
| 135 | // the relation for raw expressions": a CANONICAL expression can CONTAIN raw |
| 136 | // operands, because a lazy operator holds them un-canonicalized. So a |
| 137 | // canonical `Map(…)` holding a raw `q` compared equal to canonical `Map(…)`s |
| 138 | // from two different scopes that were themselves unequal — the transitivity |
| 139 | // bridge sat inside the domain every dedup key uses. |
| 140 | // |
| 141 | // A caller that legitimately compares a TEMPLATE against a bound subject |
| 142 | // asks for it explicitly (`sameSyntactic`) rather than relying on |
| 143 | // unboundness as an implicit signal. |
| 144 | return false; |
| 145 | } |
| 146 | |
| 147 | /** |
| 148 | * Is `def` the standard-library definition of `sym.symbol`? |
| 149 | * |
| 150 | * The engine's ROOT scope (the end of the parent chain) holds exactly the |
| 151 | * standard library; every user declaration — including a top-level one — |
| 152 | * lands in a child scope. Only reached when the two sides' definitions |
| 153 | * already differ in identity, so the chain walk is off the hot path. |
| 154 | */ |
| 155 | function isLibraryBinding( |
| 156 | sym: Expression & { symbol: string }, |
| 157 | def: BoxedBaseDefinition |
| 158 | ): boolean { |
| 159 | let scope = sym.engine.context?.lexicalScope; |
| 160 | if (scope === undefined) return false; |
| 161 | // Loose check: the chain terminates with `null` OR `undefined` depending on |
| 162 | // how the root context was built. |
| 163 | while (scope.parent != null) scope = scope.parent; |
| 164 | const binding = scope.bindings.get(sym.symbol); |
| 165 | if (binding === undefined) return false; |
| 166 | // A scope binding is a tagged record; `baseDefinition` unwraps it, so |
| 167 | // compare against both halves. (Inline — `isValueDef`/`isOperatorDef` live |
| 168 | // in `utils.ts`, whose import would close the cycle `binders.ts` broke.) |
| 169 | return ( |
| 170 | ('value' in binding && binding.value === def) || |
| 171 | ('operator' in binding && binding.operator === def) |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Structural equality that compares symbols by NAME, ignoring bindings. |
| 177 | * |
| 178 | * For comparing a TEMPLATE against a subject: a rule pattern is parsed raw so |
| 179 | * canonicalization cannot collapse its structure or mangle its wildcards, |
| 180 | * which leaves its literal symbols unbound — the `\pi` of `\pi + a -> 2a` |
| 181 | * must still match a canonical `π`. That is a question about syntax, not |
| 182 | * about which binding a symbol denotes, so it gets its own entry point. |
| 183 | */ |
| 184 | export function sameSyntactic(a: Expression, b: Expression): boolean { |
| 185 | return same(a, b, undefined, undefined, true); |
| 186 | } |
| 187 | |
| 188 | /** What a node binds: name → its definition, or `null` when the binder names |
| 189 | * the variable without owning its definition. See `boundVariableBindings`. */ |
| 190 | type BinderMap = ReadonlyMap<string, BoxedValueDefinition | null>; |
| 191 | |
| 192 | /** |
| 193 | * Does an occurrence denote the enclosing binder? |
no test coverage detected