(
ops: ReadonlyArray<Expression>,
options: { engine: ComputeEngine }
)
| 251 | // symbol `n`, or `n − 1` — cannot be enumerated: the domain is |
| 252 | // symbolic. Without this check `normalizeIndexingSet` silently |
| 253 | // substitutes its default iteration window for the unusable bound, so |
| 254 | // `Sum(k, [k, 1, n])` evaluated as if `n` were 10001 (→ 50015001). |
| 255 | const symbolicBound = (b: Expression) => |
| 256 | !(isSymbol(b) && b.symbol === 'Nothing') && |
| 257 | Number.isNaN(bigopBoundValue(b)); |
| 258 | if (symbolicBound(idx.op2) || symbolicBound(idx.op3)) return 'symbolic'; |
| 259 | if (!normalizeIndexingSet(idx).isFinite) infinite = true; |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | if (!infinite) return 'finite'; |
| 264 | |
| 265 | // The body is numeric iff its only free variables are the index variables. |
| 266 | // (`unknowns` already excludes constants like `Pi` and any name bound to a |
| 267 | // value, and function heads are not counted as free variables.) |
| 268 | const numericBody = (body?.unknowns ?? []).every((s) => indexNames.has(s)); |
| 269 | return numericBody ? 'numeric' : 'symbolic'; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Shift a body's index `k → k + 1`, returning the substituted expression. |
| 274 | */ |
| 275 | function shiftIndex( |
| 276 | expr: Expression, |
| 277 | index: string, |
| 278 | ce: ComputeEngine |
| 279 | ): Expression { |
| 280 | return expr.subs({ [index]: ce.box(['Add', index, 1]) }); |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * Decompose a telescoping body `Add(a, b)` (exactly two terms, exactly one a |
| 285 | * `Negate`) into its positive and negative parts and the orientation: |
| 286 | * - forward: body = t(k+1) − t(k) (with `t = neg`), sums to t(b+1) − t(a) |
| 287 | * - mirror: body = t(k) − t(k+1) (with `t = pos`), sums to t(a) − t(b+1) |
| 288 | * Both parts must depend on the index. Returns undefined if the body is not a |
| 289 | * `k → k+1` shift pair. |
| 290 | */ |
| 291 | function telescopingParts( |
| 292 | body: Expression, |
| 293 | index: string, |
| 294 | ce: ComputeEngine |
| 295 | ): { pos: Expression; neg: Expression; forward: boolean } | undefined { |
| 296 | if (!isFunction(body, 'Add') || body.ops.length !== 2) return undefined; |
| 297 | |
| 298 | let pos: Expression | undefined; |
| 299 | let neg: Expression | undefined; |
| 300 | for (const t of body.ops) { |
| 301 | if (isFunction(t, 'Negate')) { |
| 302 | if (neg) return undefined; // two negated terms → not a telescoping pair |
| 303 | neg = t.op1; |
| 304 | } else { |
| 305 | if (pos) return undefined; |
| 306 | pos = t; |
| 307 | } |
| 308 | } |
| 309 | if (!pos || !neg) return undefined; |
no test coverage detected