Rewrite uncollected polynomial Add factors of a (normal-form) integrand * as Σ Coeff·x^k with x-free coefficients; null when nothing changes.
( ce: ComputeEngine, integrand: Expression, variable: string )
| 772 | ce, |
| 773 | free.length === 1 ? free[0] : ce._fn('Multiply', free) |
| 774 | ); |
| 775 | const u = recanonicalize( |
| 776 | ce, |
| 777 | rest.length === 1 ? rest[0] : ce._fn('Multiply', rest) |
| 778 | ); |
| 779 | this.stats.preludeFirings++; |
| 780 | // Record the constant pull BEFORE recursing (preorder); roll it back if |
| 781 | // the residual integral fails so an aborted attempt leaves no records. |
| 782 | const mark = this.mark(); |
| 783 | this.record( |
| 784 | entryNode, |
| 785 | () => ce.function('Multiply', [c, inert(u)]), |
| 786 | 'integrate.constant-factor' |
| 787 | ); |
| 788 | const F = this.intRec(u, variable, depth + 1); |
| 789 | if (F === null) { |
| 790 | this.truncate(mark); |
| 791 | return null; |
| 792 | } |
| 793 | return c.mul(F); |
| 794 | } |
| 795 | } |
| 796 | |
| 797 | // ---- rule dispatch (priority order) ------------------------------- |
| 798 | const hooks: Hooks = { int: recurse }; |
| 799 | const trace = (id: string, stage: string): void => { |
| 800 | if (this.options.trace) this.stats.trace.push({ id, stage, depth }); |
| 801 | }; |
| 802 | // Second-level dispatch screen: the candidate list for this integrand's |
| 803 | // root operator (root-operator prescreen, cached once per operator over |
| 804 | // the bundle) narrowed by the integrand-skeleton feature set. Both |
| 805 | // filters are NECESSARY conditions on a match (see compile.ts |
| 806 | // `requiredHeads`), so no rule that could fire is dropped; rule order |
| 807 | // within the candidate list is the original priority order (stable |
| 808 | // filtering — the buckets are built by a single ordered pass). |
| 809 | const integrandHeads = new Set<string>(); |
| 810 | collectHeads(integrand, integrandHeads); |
| 811 | const noSkeleton = this.options.noSkeleton ?? NO_SKELETON; |
| 812 | const candidates = noSkeleton |
| 813 | ? this.rules |
| 814 | : this.candidatesFor(integrand.operator); |
| 815 | const dispatch = (envCap: number): Expression | null => { |
| 816 | for (const rule of candidates) { |
| 817 | if (Date.now() > this.deadline) return null; |
| 818 | if (noSkeleton) { |
| 819 | // legacy path: root-operator prescreen only (A/B baseline) |
| 820 | if (rule.rootOp !== null && rule.rootOp !== integrand.operator) |
| 821 | continue; |
| 822 | } else { |
| 823 | // integrand-skeleton screen: skip when a head the pattern provably |
| 824 | // requires is absent from the integrand (conservative — fail-open |
| 825 | // on inclusion). The candidate bucket already enforced the |
| 826 | // root-operator match. |
| 827 | const req = rule.requiredHeads; |
| 828 | let skip = false; |