Commit cf6dac0
fix(vm): combinations on lazy iterators skipped tail combinations π’ (#144)
## What
`CombinationsIter`'s pivot algorithm gave incorrect results for lazy
sources (any iterator that buffers on demand, e.g. the output of
`.enumerate()`, `.map()`, `.filter()`, etc.).
## Reproducer
```ndc
print([1, 2, 3, 4].combinations(2).list());
// [(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)] β correct
print([1, 2, 3, 4].enumerate().combinations(2).list());
// [((0,1),(1,2)),((0,1),(2,3)),((1,2),(2,3)),((1,2),(3,4)),((2,3),(3,4))]
// ^ ((0,1),(3,4)) is missing and combinations are out of lex order
```
Eager sources (a list directly) were unaffected. Lazy sources missed
combinations whose rightmost index landed in the not-yet-pulled tail.
## Cause
`CombinationsIter` keeps a growing buffer of pulled elements and a list
of `indices` into that buffer. To produce the next combination it picks
the rightmost index that can still advance within the current buffer:
```rust
let pivot = match (0..k).rev().find(|&i| self.indices[i] < pool_len - k + i) { ... };
```
For an eager source, `pool_len` is the final size and the check is
correct. For a lazy source, `pool_len` is "what's been pulled so far".
When `indices[k-1]` reaches the end of the current buffer, the algorithm
decides "no room to advance the rightmost index" and falls back to
advancing an earlier index β even though one more pull would have
unlocked it.
The existing code did try to pull more, but only in the fallback `pivot
= None` branch, which only fires when *every* index is at its
current-pool maximum. In our trace above the algorithm picks pivot = 0
before reaching that fallback.
## Fix
Pull `indices[k - 1] + 1` worth of buffer *before* computing the pivot.
If the source still has elements, `pool_len` grows and the rightmost
index pivots as expected. If the source is exhausted the buffer stays
put and the existing pivot logic correctly falls back to an earlier
index.
## Tests
- New
`tests/functional/programs/900_bugs/bug0021_combinations_lazy_source.ndc`
covering the original 4-element case, the larger k = 3 case, and the
eager-source baseline.
- All 287 existing functional tests pass.
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>1 parent 3f2dbc6 commit cf6dac0
2 files changed
Lines changed: 41 additions & 15 deletions
File tree
- ndc_vm/src
- tests/functional/programs/900_bugs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
462 | 462 | | |
463 | 463 | | |
464 | 464 | | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
465 | 477 | | |
466 | 478 | | |
467 | 479 | | |
468 | 480 | | |
469 | 481 | | |
470 | 482 | | |
471 | 483 | | |
472 | | - | |
473 | | - | |
474 | | - | |
475 | | - | |
476 | | - | |
477 | | - | |
478 | | - | |
479 | | - | |
480 | | - | |
481 | | - | |
482 | | - | |
| 484 | + | |
483 | 485 | | |
484 | 486 | | |
485 | 487 | | |
486 | 488 | | |
487 | 489 | | |
488 | 490 | | |
489 | | - | |
490 | | - | |
491 | | - | |
492 | | - | |
493 | 491 | | |
494 | 492 | | |
495 | 493 | | |
| |||
Lines changed: 28 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
0 commit comments