Add itertools adaptors: takewhile, dropwhile, filterfalse, starmap - #657
Add itertools adaptors: takewhile, dropwhile, filterfalse, starmap#657rewitt94 wants to merge 10 commits into
itertools adaptors: takewhile, dropwhile, filterfalse, starmap#657Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
1 issue found across 19 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/monty/src/types/itertools/predicate.rs">
<violation number="1" location="crates/monty/src/types/itertools/predicate.rs:16">
P3: Predicate invocation now has two semantically identical ownership and evaluation paths, so future fixes to callable errors or heap cleanup can diverge between `filter()` and these adaptors. Centralizing this operation and reusing it from `builtin_filter` would keep the behavior consistent.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| /// (an external function, an `os` call) is rejected rather than paused; `ctx` | ||
| /// names the adaptor in that error. | ||
| pub(super) fn call_predicate(predicate: &Value, item: &Value, ctx: &'static str, vm: &mut VM<'_>) -> RunResult<bool> { | ||
| let arg = item.clone_with_heap(vm.heap); |
There was a problem hiding this comment.
P3: Predicate invocation now has two semantically identical ownership and evaluation paths, so future fixes to callable errors or heap cleanup can diverge between filter() and these adaptors. Centralizing this operation and reusing it from builtin_filter would keep the behavior consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty/src/types/itertools/predicate.rs, line 16:
<comment>Predicate invocation now has two semantically identical ownership and evaluation paths, so future fixes to callable errors or heap cleanup can diverge between `filter()` and these adaptors. Centralizing this operation and reusing it from `builtin_filter` would keep the behavior consistent.</comment>
<file context>
@@ -0,0 +1,21 @@
+/// (an external function, an `os` call) is rejected rather than paused; `ctx`
+/// names the adaptor in that error.
+pub(super) fn call_predicate(predicate: &Value, item: &Value, ctx: &'static str, vm: &mut VM<'_>) -> RunResult<bool> {
+ let arg = item.clone_with_heap(vm.heap);
+ let result = vm.evaluate_function(ctx, predicate, ArgValues::One(arg))?;
+ let truthy = result.py_bool(vm);
</file context>
There was a problem hiding this comment.
Done — builtin_filter now calls the shared helper. predicate.rs moves to the crate root (crate::predicate) and call_predicate becomes pub(crate), so builtins/ does not reach into types::itertools::.
d9b18dc to
97af3ef
Compare
`Type::is_iterator` lists every other concrete iterator, including the seven
earlier `itertools` ones, and the four new variants were not added to it. The
protocol check for a user-defined `__iter__` consults it, so returning one of
them from `__iter__` was rejected:
class Wrapped:
def __iter__(self):
return itertools.takewhile(lambda x: x < 3, [1, 2, 3])
list(Wrapped())
# CPython: [1, 2]
# Monty: TypeError: iter() returned non-iterator of type 'itertools.takewhile'
`pairwise` in the same position already worked, which is what makes it a
classification gap rather than anything to do with these adaptors' behaviour.
Reported by cubic on pydantic#657.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two guard assertions stopped at the first item, before the source's temporary `StopIteration`, so they passed whether or not either adaptor treated exhaustion as terminal — which is the regression they exist to catch. Both are now driven through the exception and asserted on the item after it. Reported by cubic on pydantic#657. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 19 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Two gaps cubic found on pydantic#657, both cases of a test that does not check what the code claims: `starmap` has no spent flag, so a transient `StopIteration` from its source must not finish it — nothing covered that, and an implementation that latched would have passed. `StutteringPairs` drives it through the exception and asserts the item after it. `takewhile`'s latch is documented as stopping the source being touched, not just the predicate being called, and only the predicate half was asserted. `Counting` records how often it was asked, so the second drain proves the source is left alone rather than merely yielding nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Type::is_iterator` lists every other concrete iterator, including the seven
earlier `itertools` ones, and the four new variants were not added to it. The
protocol check for a user-defined `__iter__` consults it, so returning one of
them from `__iter__` was rejected:
class Wrapped:
def __iter__(self):
return itertools.takewhile(lambda x: x < 3, [1, 2, 3])
list(Wrapped())
# CPython: [1, 2]
# Monty: TypeError: iter() returned non-iterator of type 'itertools.takewhile'
`pairwise` in the same position already worked, which is what makes it a
classification gap rather than anything to do with these adaptors' behaviour.
Reported by cubic on pydantic#657.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two guard assertions stopped at the first item, before the source's temporary `StopIteration`, so they passed whether or not either adaptor treated exhaustion as terminal — which is the regression they exist to catch. Both are now driven through the exception and asserted on the item after it. Reported by cubic on pydantic#657. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps cubic found on pydantic#657, both cases of a test that does not check what the code claims: `starmap` has no spent flag, so a transient `StopIteration` from its source must not finish it — nothing covered that, and an implementation that latched would have passed. `StutteringPairs` drives it through the exception and asserts the item after it. `takewhile`'s latch is documented as stopping the source being touched, not just the predicate being called, and only the predicate half was asserted. `Counting` records how often it was asked, so the second drain proves the source is left alone rather than merely yielding nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `call_predicate` — new in this PR, and used by `takewhile`, `dropwhile` and `filterfalse` — duplicates the predicate call already in `builtin_filter`, so a future fix to callable errors or heap cleanup could diverge between the two. `builtin_filter` is pre-existing and otherwise untouched by this branch, so this diff is purely that retrofit. It is behaviour-neutral: the hand-rolled `py_bool` -> `drop_with` -> `is_truthy?` and `call_predicate`'s `defer_drop!` release the result on exactly the same paths, and `evaluate_function` consumes the `ArgValues` either way. `predicate.rs` moves to the crate root so `builtins/` need not reach into `types::itertools::`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cd8b45f to
e5dc968
Compare
| let is_truthy = result.py_bool(vm); | ||
| result.drop_with(vm); | ||
| is_truthy? | ||
| call_predicate(function, item, "filter()", vm)? |
There was a problem hiding this comment.
This diff is here because cubic asked for it (see the thread on predicate.rs). builtin_filter is pre-existing and otherwise untouched by this branch — the change routes it through call_predicate, the helper this PR adds for takewhile/dropwhile/filterfalse, so the two predicate-call paths cannot drift apart. Behaviour-neutral.
cubic asked for this on pydantic#657: releasing the predicate on its first rejection diverged from CPython, which holds `lz->func` for the iterator's whole life. Nothing in sandboxed Python can observe the difference today — there is no `__del__`, no weakrefs, no `getrefcount` — but the divergence would become visible the moment any finalizer lands, so match CPython now. `DropWhile` swaps `predicate: Option<Value>` for `predicate: Value` plus a `dropping` flag; the predicate is cloned only while it is still consulted, and the rejection branch clears the flag rather than releasing. `refcount__itertools_adaptors.py` asserted the old lifetime explicitly, so its `drop_pred` expectation moves from 1 to 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // TODO: when this fails, box the offending variant (`GroupBy(Box<GroupBy>)`), | ||
| // not the enum and not at the `HeapData` boundary. | ||
| #[cfg(target_pointer_width = "64")] | ||
| const _: () = assert!(mem::size_of::<ItertoolsIter>() <= mem::size_of::<Dict>()); |
There was a problem hiding this comment.
We will need to box ItertoolsIter soon as per #636 - this is just a compile check to remind
cubic asked for this on pydantic#657: `takewhile`, `dropwhile` and `filterfalse` each opened `next` with the same clone-guard-fetch-guard scaffolding, so the refcount-critical part — guarding the item across a test that may raise — was maintained in three copies. `step::next_tested` now owns the predicate and source clones, the fetch and the item guard, runs the caller's test inside the guarded region and hands the item back owned; `step::next_item` covers `dropwhile`'s untested tail. Each adaptor keeps only its own decision, which is all that actually differed. The `Value::None` truth test stays at `filterfalse`'s call site deliberately: folding it into the shared helper would make `takewhile(None, ...)` silently truth-test rather than raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Type::is_iterator` lists every other concrete iterator, including the seven
earlier `itertools` ones, and the four new variants were not added to it. The
protocol check for a user-defined `__iter__` consults it, so returning one of
them from `__iter__` was rejected:
class Wrapped:
def __iter__(self):
return itertools.takewhile(lambda x: x < 3, [1, 2, 3])
list(Wrapped())
# CPython: [1, 2]
# Monty: TypeError: iter() returned non-iterator of type 'itertools.takewhile'
`pairwise` in the same position already worked, which is what makes it a
classification gap rather than anything to do with these adaptors' behaviour.
Reported by cubic on pydantic#657.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two guard assertions stopped at the first item, before the source's temporary `StopIteration`, so they passed whether or not either adaptor treated exhaustion as terminal — which is the regression they exist to catch. Both are now driven through the exception and asserted on the item after it. Reported by cubic on pydantic#657. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74d9c0d to
594a863
Compare
Two gaps cubic found on pydantic#657, both cases of a test that does not check what the code claims: `starmap` has no spent flag, so a transient `StopIteration` from its source must not finish it — nothing covered that, and an implementation that latched would have passed. `StutteringPairs` drives it through the exception and asserts the item after it. `takewhile`'s latch is documented as stopping the source being touched, not just the predicate being called, and only the predicate half was asserted. `Counting` records how often it was asked, so the second drain proves the source is left alone rather than merely yielding nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `call_predicate` — new in this PR, and used by `takewhile`, `dropwhile` and `filterfalse` — duplicates the predicate call already in `builtin_filter`, so a future fix to callable errors or heap cleanup could diverge between the two. `builtin_filter` is pre-existing and otherwise untouched by this branch, so this diff is purely that retrofit. It is behaviour-neutral: the hand-rolled `py_bool` -> `drop_with` -> `is_truthy?` and `call_predicate`'s `defer_drop!` release the result on exactly the same paths, and `evaluate_function` consumes the `ArgValues` either way. `predicate.rs` moves to the crate root so `builtins/` need not reach into `types::itertools::`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: releasing the predicate on its first rejection diverged from CPython, which holds `lz->func` for the iterator's whole life. Nothing in sandboxed Python can observe the difference today — there is no `__del__`, no weakrefs, no `getrefcount` — but the divergence would become visible the moment any finalizer lands, so match CPython now. `DropWhile` swaps `predicate: Option<Value>` for `predicate: Value` plus a `dropping` flag; the predicate is cloned only while it is still consulted, and the rejection branch clears the flag rather than releasing. `refcount__itertools_adaptors.py` asserted the old lifetime explicitly, so its `drop_pred` expectation moves from 1 to 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `takewhile`, `dropwhile` and `filterfalse` each opened `next` with the same clone-guard-fetch-guard scaffolding, so the refcount-critical part — guarding the item across a test that may raise — was maintained in three copies. `step::next_tested` now owns the predicate and source clones, the fetch and the item guard, runs the caller's test inside the guarded region and hands the item back owned; `step::next_item` covers `dropwhile`'s untested tail. Each adaptor keeps only its own decision, which is all that actually differed. The `Value::None` truth test stays at `filterfalse`'s call site deliberately: folding it into the shared helper would make `takewhile(None, ...)` silently truth-test rather than raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Close and open to re-trigger crashed CI |
`Type::is_iterator` lists every other concrete iterator, including the seven
earlier `itertools` ones, and the four new variants were not added to it. The
protocol check for a user-defined `__iter__` consults it, so returning one of
them from `__iter__` was rejected:
class Wrapped:
def __iter__(self):
return itertools.takewhile(lambda x: x < 3, [1, 2, 3])
list(Wrapped())
# CPython: [1, 2]
# Monty: TypeError: iter() returned non-iterator of type 'itertools.takewhile'
`pairwise` in the same position already worked, which is what makes it a
classification gap rather than anything to do with these adaptors' behaviour.
Reported by cubic on pydantic#657.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two guard assertions stopped at the first item, before the source's temporary `StopIteration`, so they passed whether or not either adaptor treated exhaustion as terminal — which is the regression they exist to catch. Both are now driven through the exception and asserted on the item after it. Reported by cubic on pydantic#657. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps cubic found on pydantic#657, both cases of a test that does not check what the code claims: `starmap` has no spent flag, so a transient `StopIteration` from its source must not finish it — nothing covered that, and an implementation that latched would have passed. `StutteringPairs` drives it through the exception and asserts the item after it. `takewhile`'s latch is documented as stopping the source being touched, not just the predicate being called, and only the predicate half was asserted. `Counting` records how often it was asked, so the second drain proves the source is left alone rather than merely yielding nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `call_predicate` — new in this PR, and used by `takewhile`, `dropwhile` and `filterfalse` — duplicates the predicate call already in `builtin_filter`, so a future fix to callable errors or heap cleanup could diverge between the two. `builtin_filter` is pre-existing and otherwise untouched by this branch, so this diff is purely that retrofit. It is behaviour-neutral: the hand-rolled `py_bool` -> `drop_with` -> `is_truthy?` and `call_predicate`'s `defer_drop!` release the result on exactly the same paths, and `evaluate_function` consumes the `ArgValues` either way. `predicate.rs` moves to the crate root so `builtins/` need not reach into `types::itertools::`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
594a863 to
2bd9f37
Compare
cubic asked for this on pydantic#657: releasing the predicate on its first rejection diverged from CPython, which holds `lz->func` for the iterator's whole life. Nothing in sandboxed Python can observe the difference today — there is no `__del__`, no weakrefs, no `getrefcount` — but the divergence would become visible the moment any finalizer lands, so match CPython now. `DropWhile` swaps `predicate: Option<Value>` for `predicate: Value` plus a `dropping` flag; the predicate is cloned only while it is still consulted, and the rejection branch clears the flag rather than releasing. `refcount__itertools_adaptors.py` asserted the old lifetime explicitly, so its `drop_pred` expectation moves from 1 to 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `takewhile`, `dropwhile` and `filterfalse` each opened `next` with the same clone-guard-fetch-guard scaffolding, so the refcount-critical part — guarding the item across a test that may raise — was maintained in three copies. `step::next_tested` now owns the predicate and source clones, the fetch and the item guard, runs the caller's test inside the guarded region and hands the item back owned; `step::next_item` covers `dropwhile`'s untested tail. Each adaptor keeps only its own decision, which is all that actually differed. The `Value::None` truth test stays at `filterfalse`'s call site deliberately: folding it into the shared helper would make `takewhile(None, ...)` silently truth-test rather than raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
…tarmap` Four more callables on the source-wrapping foundation from pydantic#635. All four apply a **user callable per item**, which none of the earlier adaptors do — `next` re-enters the VM through `evaluate_function`, so the item under test is held in a `DropGuard` across the call and the callable is a second GC edge alongside the source. Latching differs per adaptor and is user-visible. `takewhile` latches on the first rejection — and only that: CPython's `takewhile_next` sets `stop` when the predicate fails, while an exhausted source leaves it free to be driven again, which a hand-written `__next__` that raises `StopIteration` and then resumes can observe. `dropwhile` clears its dropping state on the first failure and yields the rest untested. `filterfalse` and `starmap` never latch at all. Each adaptor releases what it can no longer reach at that point rather than at destruction, as `pairwise`/`islice` do: `takewhile` drops both predicate and source when it latches, `dropwhile` drops the predicate it will never call again. `filterfalse` and `starmap` consult both on every `next`, so neither releases early. `filterfalse` shares the one-argument application in `predicate.rs` with the two `while` adaptors and adds the truth-test path `filter(None, ...)` uses. `starmap` packs each item into `ArgValues::Empty`/`One`/`Two` rather than `ArgsKargs`, because extractors such as `get_one_arg` match the shape structurally. A callable that suspends is rejected, not paused: `evaluate_function` runs a frame to completion and cannot yield to the host, so one reaching an external function raises `NotImplementedError`. Documented in `limitations/itertools.md` and covered Rust-side for both call sites — the shared predicate helper and `starmap`'s own. `ItertoolsIter` gains a compile-time budget against `Dict`, the widest `HeapData` payload: the enum is memcpy'd inline on every allocate and free, and against `Islice`/`Chain`'s 56, so the enum stays at 64. Compatible dump change: new `StaticStrings`, `Type`, `MontyType` and `ItertoolsIter` variants are appended, so only the fingerprint moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Type::is_iterator` lists every other concrete iterator, including the seven
earlier `itertools` ones, and the four new variants were not added to it. The
protocol check for a user-defined `__iter__` consults it, so returning one of
them from `__iter__` was rejected:
class Wrapped:
def __iter__(self):
return itertools.takewhile(lambda x: x < 3, [1, 2, 3])
list(Wrapped())
# CPython: [1, 2]
# Monty: TypeError: iter() returned non-iterator of type 'itertools.takewhile'
`pairwise` in the same position already worked, which is what makes it a
classification gap rather than anything to do with these adaptors' behaviour.
Reported by cubic on pydantic#657.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two guard assertions stopped at the first item, before the source's temporary `StopIteration`, so they passed whether or not either adaptor treated exhaustion as terminal — which is the regression they exist to catch. Both are now driven through the exception and asserted on the item after it. Reported by cubic on pydantic#657. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps cubic found on pydantic#657, both cases of a test that does not check what the code claims: `starmap` has no spent flag, so a transient `StopIteration` from its source must not finish it — nothing covered that, and an implementation that latched would have passed. `StutteringPairs` drives it through the exception and asserts the item after it. `takewhile`'s latch is documented as stopping the source being touched, not just the predicate being called, and only the predicate half was asserted. `Counting` records how often it was asked, so the second drain proves the source is left alone rather than merely yielding nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The assertion broke the wasm build: `Dict` is the widest `HeapData` payload on 64-bit, but on `wasm32-wasip1` it halves to 36 bytes while the adaptors' `i64` fields do not, leaving `ItertoolsIter` at 48 — over a budget that no longer means anything there. `HeapData` is 64 bytes on that target and set by other variants, so the family is not what it has to stay under. Gate the assertion (and the `Dict` import it needs) on 64-bit, where pydantic#636's layout work applies and where the hosts that run the heap hot path live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `call_predicate` — new in this PR, and used by `takewhile`, `dropwhile` and `filterfalse` — duplicates the predicate call already in `builtin_filter`, so a future fix to callable errors or heap cleanup could diverge between the two. `builtin_filter` is pre-existing and otherwise untouched by this branch, so this diff is purely that retrofit. It is behaviour-neutral: the hand-rolled `py_bool` -> `drop_with` -> `is_truthy?` and `call_predicate`'s `defer_drop!` release the result on exactly the same paths, and `evaluate_function` consumes the `ArgValues` either way. `predicate.rs` moves to the crate root so `builtins/` need not reach into `types::itertools::`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: releasing the predicate on its first rejection diverged from CPython, which holds `lz->func` for the iterator's whole life. Nothing in sandboxed Python can observe the difference today — there is no `__del__`, no weakrefs, no `getrefcount` — but the divergence would become visible the moment any finalizer lands, so match CPython now. `DropWhile` swaps `predicate: Option<Value>` for `predicate: Value` plus a `dropping` flag; the predicate is cloned only while it is still consulted, and the rejection branch clears the flag rather than releasing. `refcount__itertools_adaptors.py` asserted the old lifetime explicitly, so its `drop_pred` expectation moves from 1 to 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cubic asked for this on pydantic#657: `takewhile`, `dropwhile` and `filterfalse` each opened `next` with the same clone-guard-fetch-guard scaffolding, so the refcount-critical part — guarding the item across a test that may raise — was maintained in three copies. `step::next_tested` now owns the predicate and source clones, the fetch and the item guard, runs the caller's test inside the guarded region and hands the item back owned; `step::next_item` covers `dropwhile`'s untested tail. Each adaptor keeps only its own decision, which is all that actually differed. The `Value::None` truth test stays at `filterfalse`'s call site deliberately: folding it into the shared helper would make `takewhile(None, ...)` silently truth-test rather than raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebase fallout from pydantic#668, which moved memory accounting to the global allocator: `Heap::allocate` is now infallible, so the four constructors drop their `?`, and `ItertoolsIter::buffered_size` goes with the rest of the `py_estimate_size` machinery — `chain` and `cycle` no longer charge their buffers here because the allocator meters them directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`next(itertools.dropwhile(bool, itertools.count(1)))` ran forever with `max_duration` set. The VM's dispatch checkpoint is per-`run()`, so a native loop driving a native source reaches none — and a user-defined predicate is no safer, since a body under `CHECK_INTERVAL` instructions restarts the countdown rather than reaching it. `VM::run` already states the contract: a native loop calling a shorter callback "must poll the tracker itself". Adds an amortized `check_time_every` to every adaptor loop that can discard without yielding: `dropwhile` and `filterfalse` here, and `compress`, `islice` and `chain`, which have the same hole today. `starmap` spreads its item through the shared iterator drain, which checked size per item but never time, so that gains a poll too — fixing every other eager drain with it. `takewhile` needs nothing: it tests one item per call and returns, so the dispatch checkpoint still bounds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0734a85 to
e865ed0
Compare
Four more
itertoolscallables on the source-wrapping foundation from #635 (now merged, so this is rebased ontomain):takewhile,dropwhile,filterfalseandstarmap. All four apply a user callable per item, whichpairwise/compress/islice/chain/cycledo not — that is the new capability and the main thing to review.Applying the callable re-enters the VM.
nextcallsevaluate_function, which can raise or allocate, so the item under test is held in aDropGuardacross the call: it is yielded on one answer and dropped on the other. The callable is also a second GC edge alongside the source, sofor_each_child_idandpy_dec_ref_idstrace both.Latching differs per adaptor and is user-visible.
takewhilelatchesdoneon the first rejection, so neither the predicate nor the source is touched again.dropwhileclearsdroppingon the first failure and yields everything after it untested.filterfalseandstarmaphave no spent flag at all — source exhaustion is their only end condition.filterfalseacceptsNone. It shares the one-argument application inpredicate.rswith the twowhileadaptors, plus the truth-test pathfilter(None, ...)uses.Spreading is arity-sensitive.
starmappacks each item intoArgValues::Empty/One/Tworather thanArgsKargs, because extractors such asget_one_argmatch the shape structurally — a one-elementArgsKargsis rejected as the wrong shape (abs() takes exactly one argument) even though the arity is right.A callable that suspends is rejected, not paused.
evaluate_functionruns a frame to completion and cannot yield to the host, so a callable reaching an external function or anosoperation raisesNotImplementedErrorwhere CPython would simply call it — the same restriction as__init__/__next__/__repr__. Documented inlimitations/itertools.mdand covered by a Rust-side test.Heap footprint. The four new variants are 32–40 bytes, under
Islice/Chain's 56, soItertoolsIterstays at 64 andHeapDataat 72 — inside the 80-byte ceiling from #636.Not implemented:
accumulate,batched,combinations,combinations_with_replacement,groupby,permutations,product,tee,zip_longest.Compatible dump change: new
StaticStrings,Type,MontyTypeandItertoolsItervariants are appended, so only the fingerprint moves.Summary by cubic
Adds
itertoolsadaptorstakewhile,dropwhile,filterfalse, andstarmap, and enforcesmax_durationin native adaptor loops and eager drains. Previously, discarding loops and drains could spin; now they poll the tracker. The new adaptors execute a per-item callable and are classified as concrete iterators.evaluate_function; external/host callables raiseNotImplementedErrornaming the adaptor.filterfalse(None, iterable)keeps falsy items.starmapspreads each item into arguments; a non-iterable item raisesTypeError. All four take exactly two positional-only args and resolve the iterable eagerly; typeshed stubs updated.takewhilelatches on first predicate failure, releases its predicate and source, and does not latch on transientStopIteration.dropwhilestops dropping after the first failure, yields the rest untested, and keeps its predicate owned until destruction.filterfalseandstarmapnever latch and re-drive past transientStopIteration.dropwhile/filterfalseand inchain/compress/islicepoll the tracker; eager drains, includingstarmapand shared iterator drains, check time per yield. Overhead is amortized; behavior otherwise unchanged.predicate::call_predicateand routedfilter()through it. NewType/MontyType/StaticStrings/ItertoolsItervariants added; adaptors treated as iterator types; dump fingerprint bumped. TheItertoolsItersize-budget assertion is limited to 64‑bit hosts.Written for commit e865ed0. Summary will update on new commits.