(
integrand: Expression,
variable: string,
depth: number
)
| 310 | // `Integrate`, which re-enters the provider → this driver; the flag makes |
| 311 | // that re-entrant `int()` bail immediately so it cannot clobber the outer |
| 312 | // call's deadline/memo. |
| 313 | private templateBuilding = false; |
| 314 | readonly stats: DriverStats = { |
| 315 | calls: 0, |
| 316 | ruleFirings: {}, |
| 317 | preludeFirings: 0, |
| 318 | failures: 0, |
| 319 | trace: [], |
| 320 | }; |
| 321 | |
| 322 | constructor( |
| 323 | private readonly ce: ComputeEngine, |
| 324 | private readonly rules: CompiledRule[], |
| 325 | private readonly options: { |
| 326 | timeLimitMs?: number; |
| 327 | trace?: boolean; |
| 328 | /** @internal Force the legacy full-scan dispatch (root-operator |
| 329 | * prescreen only), bypassing the skeleton screen. Overrides the |
| 330 | * `RUBI_NO_SKELETON` env default. For the A/B equivalence harness. */ |
| 331 | noSkeleton?: boolean; |
| 332 | } = {} |
| 333 | ) {} |
| 334 | |
| 335 | // Per-root-operator candidate lists: the rules whose root-operator prescreen |
| 336 | // admits an integrand with that operator (rootOp === op, plus the wildcard |
| 337 | // rootOp === null rules that can match any root). Built lazily and cached — |
| 338 | // the operator alphabet is tiny (Multiply/Power/a few heads), so this is |
| 339 | // effectively "once per bundle". Each list is a stable subsequence of the |
| 340 | // ordered `this.rules`, preserving the original priority order exactly. |
| 341 | private readonly candidateCache = new Map<string, CompiledRule[]>(); |
| 342 | private candidatesFor(operator: string): CompiledRule[] { |
| 343 | let list = this.candidateCache.get(operator); |
| 344 | if (list === undefined) { |
| 345 | list = this.rules.filter( |
| 346 | (r) => r.rootOp === null || r.rootOp === operator |
| 347 | ); |
| 348 | this.candidateCache.set(operator, list); |
| 349 | } |
| 350 | return list; |
| 351 | } |
| 352 | |
| 353 | // ── step recording (active only when `this.records` is set) ──────────── |
| 354 | /** The current `records` length, or 0 when not tracing. Capture before an |
| 355 | * attempt so it can be rolled back with `truncate` if the attempt fails. */ |
| 356 | private mark(): number { |
| 357 | return this.records?.length ?? 0; |
| 358 | } |
| 359 | /** Drop every record pushed since `mark` (attempt-scoped rollback). */ |
| 360 | private truncate(mark: number): void { |
| 361 | if (this.records) this.records.length = mark; |
| 362 | } |
| 363 | /** Push a step record, returning its index (or -1 when not recording). |
| 364 | * `node`/`replacement` are thunks so their (sometimes non-trivial) |
| 365 | * construction is skipped entirely on the hot, non-tracing path. */ |
| 366 | private record( |
| 367 | node: () => Expression, |
| 368 | replacement: () => Expression | null, |
| 369 | because: string |
no test coverage detected