( a: Expression, b: number | Expression )
| 258 | * compared equal to canonical `Map(…)`s from two different scopes that were |
| 259 | * themselves unequal. The bridge sat inside the domain every dedup key uses |
| 260 | * (`Terms.find`'s like-term collection, the assumptions `ExpressionMap`). |
| 261 | * |
| 262 | * Comparing a TEMPLATE against a subject — a rule pattern is raw by |
| 263 | * necessity — is now an explicit mode (`sameSyntactic`) rather than an |
| 264 | * implicit consequence of unboundness. |
| 265 | */ |
| 266 | export function same( |
| 267 | a: Expression, |
| 268 | b: Expression, |
| 269 | boundA?: BinderMap, |
| 270 | boundB?: BinderMap, |
| 271 | syntactic = false |
| 272 | ): boolean { |
| 273 | if (a === b) return true; |
| 274 | |
| 275 | // An OBJECT is compared by reference identity, unconditionally: it is the |
| 276 | // one mutable kind, so two objects that are equal by contents now can differ |
| 277 | // a moment later, and "are these the same object" is the only question whose |
| 278 | // answer stays true. Reaching the structural branches below would answer |
| 279 | // `true` for two distinct objects with equal slots. Having already failed |
| 280 | // the `a === b` fast path, the answer here is always `false`; it is written |
| 281 | // as the identity comparison because that is the rule, not the outcome. |
| 282 | if (isObject(a) || isObject(b)) |
| 283 | return (a as Expression) === (b as Expression); |
| 284 | |
| 285 | // A symbol is compared as a symbol, never as its value: exactly one operand |
| 286 | // being a symbol falls through to the type-mismatch branches below and is |
| 287 | // `false`. |
| 288 | |
| 289 | // |
| 290 | // BoxedFunction |
| 291 | // Operator and operands must match |
| 292 | // |
| 293 | if (isFunction(a)) { |
| 294 | if (a.operator !== b.operator) return false; |
| 295 | if (!isFunction(b)) return false; |
| 296 | if (a.nops !== b.nops) return false; |
| 297 | // What this node binds shadows any outer binding of the same name for the |
| 298 | // whole subtree. Tracked PER SIDE: `a` and `b` mint their own definitions |
| 299 | // for the same bound variable (re-boxing does exactly that), so a single |
| 300 | // shared set would be asymmetric — `same(a,b)` could differ from |
| 301 | // `same(b,a)`, breaking the equivalence relation this is a key for. |
| 302 | const innerA = extendBinders(boundA, boundVariableBindings(a)); |
| 303 | const innerB = extendBinders(boundB, boundVariableBindings(b)); |
| 304 | return a.ops.every((op, i) => |
| 305 | same(op, b.ops[i], innerA, innerB, syntactic) |
| 306 | ); |
| 307 | } |
| 308 | |
| 309 | // |
| 310 | // BoxedNumber |
| 311 | // |
| 312 | if (isNumber(a)) { |
| 313 | if (!isNumber(b)) return false; |
| 314 | const av = a.numericValue; |
| 315 | const bv = b.numericValue; |
| 316 | if (av === bv) return true; |
| 317 | // Two NaN literals are structurally the same number leaf, whether or not |
no test coverage detected