* Recursive nested iteration over Element clauses, producing a flat list of * body evaluations. Stops early on Break/Return by mutating `state.stopped`.
(
body: Expression,
elements: ReadonlyArray<Expression>,
index: number,
ce: ComputeEngine,
results: Expression[],
state: {
stopped: boolean;
broke: boolean;
value?: Expression;
count: number;
}
)
| 386 | // order — since 2026-08-15 `And` is a short-circuit form and no |
| 387 | // longer sorts; see `canonicalShortCircuit` in `logic.ts`). |
| 388 | // Building with `_fn` skipped canonicalization altogether, which |
| 389 | // at the time (when `And` still sorted) left the conjunction in |
| 390 | // authored order while `.json` (which goes through `structural`) |
| 391 | // sorted — so `x{a}{b}` and `x{b}{a}` serialized to identical |
| 392 | // MathJSON while `isSame`/`hash` disagreed, and `ce.box(e.json)` |
| 393 | // was not `isSame` to `e` (Tycho item-153 seed, the 11 |
| 394 | // `differs-json-equal` rows). Every view of the tree must come |
| 395 | // from the same canonical construction. |
| 396 | return ce._fn('When', [ |
| 397 | inner, |
| 398 | ce.function('And', [innerCond, cond.canonical]), |
| 399 | ]); |
| 400 | } |
| 401 | return ce._fn('When', [expr.canonical, cond.canonical]); |
| 402 | }, |
| 403 | evaluate: ([expr, cond], options) => { |
| 404 | const ce = options.engine; |
| 405 | const c = cond.evaluate(); |
| 406 | |
| 407 | // Desmos-style broadcast: a finite indexed collection of booleans |
| 408 | // masks element-by-element (one masked branch per element). This |
| 409 | // mirrors the boolean-mask branch of `At` in `collections.ts`. Lazy |
| 410 | // operators bypass the generic broadcast machinery, so handle it here. |
| 411 | if (c.isCollection && c.isFiniteCollection) { |
| 412 | const conds = Array.from(c.each()) as Expression[]; |
| 413 | // A cell is a boolean condition when it types `boolean` — or |
| 414 | // `broadcastable<boolean>`, the type of a comparison whose broadcast |
| 415 | // outcome is not statically settled (`h(x) ≤ 1` with `h` |
| 416 | // undeclared; see `comparisonResultType`, |
| 417 | // `library/relational-operator.ts`). Such a cell is a scalar |
| 418 | // relation here (it is one element of the materialized mask) and |
| 419 | // is held exactly like an undecided `boolean` one; gating on |
| 420 | // `boolean` alone left `x{h(x) ≤ [1,2,3]}` un-broadcast as |
| 421 | // `When(x, [h(x) ≤ 1, …])`. |
| 422 | if ( |
| 423 | conds.length > 0 && |
| 424 | conds.every( |
| 425 | (ci) => |
| 426 | ci.type.matches('boolean') || possiblyElementwiseCondition([ci]) |
| 427 | ) |
| 428 | ) { |
| 429 | // If `expr` itself evaluates to a finite indexed collection, zip |
| 430 | // elementwise (expr_i masked by c_i); otherwise mask the scalar |
| 431 | // `expr` by each c_i. Different lengths truncate to the shorter, |
| 432 | // matching `At`'s mask alignment. |
| 433 | const ev = expr.evaluate(options); |
| 434 | const zip = ev.isCollection && ev.isFiniteCollection; |
| 435 | const elems = zip ? (Array.from(ev.each()) as Expression[]) : []; |
| 436 | const n = zip ? Math.min(conds.length, elems.length) : conds.length; |
| 437 | const result: Expression[] = []; |
| 438 | for (let i = 0; i < n; i++) { |
| 439 | const ci = conds[i]; |
| 440 | const cis = sym(ci); |
| 441 | // The per-element expression: the zipped element, or the scalar. |
| 442 | const elem = zip ? elems[i] : ev; |
| 443 | if (cis === 'True') result.push(elem); |
| 444 | else if (cis === 'False') result.push(ce.symbol('Undefined')); |
| 445 | // Indeterminate (symbolic boolean): hold `When` on the element. |