Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e71a5fd
Correct what the non-unix frame stack is for
youknowone Aug 17, 2026
e981e03
Seek by the whole file position on Windows
youknowone Aug 17, 2026
9b40373
memoryview: tell a value of the wrong kind from one that does not fit
youknowone Aug 17, 2026
562dd86
Raise MemoryError for allocations sized by Python input
youknowone Aug 17, 2026
6b9b80d
Collect with the count in the object header
youknowone Aug 18, 2026
4573909
Take an explicit thread stack size as a floor in debug builds
youknowone Aug 18, 2026
b077f20
Check the native stack on every frame entry
youknowone Aug 18, 2026
45ce8f4
Take an iterable's length hint when a list is filled from it
youknowone Aug 18, 2026
c7e2658
Drop map.__length_hint__
youknowone Aug 18, 2026
94dc4db
Ask for a length hint where each caller asks for it
youknowone Aug 18, 2026
5a9c726
Settle product's pool count before it reads its arguments
youknowone Aug 18, 2026
4df44e8
Let the bool format answer with the error its value raised
youknowone Aug 18, 2026
aff3fca
Release a cell's old value after the lock
youknowone Aug 18, 2026
8f8382c
Have a set iterator hold the set it iterates
youknowone Aug 18, 2026
1c7899d
Release an exhausted iterator's container after the lock
youknowone Aug 18, 2026
800c2d8
Unskip test_free_after_iterating
youknowone Aug 18, 2026
da37fb8
Stop listing test_set as an environment polluter
youknowone Aug 18, 2026
c0faf2d
Drop winsound imports left unused
youknowone Aug 18, 2026
b2ad9b1
Ask nothing where the caller takes no room
youknowone Aug 18, 2026
3613b89
Stop asking an iterator how long it is to walk it
youknowone Aug 19, 2026
9f3580b
Name the matrix jobs rather than let the matrix name them
youknowone Aug 19, 2026
e07987d
Keep a sequence iterator active when an element raises
youknowone Aug 19, 2026
357ed5e
Name the caller in the bytes conversion TypeError
youknowone Aug 19, 2026
468f9dd
Take a strong count too large to hold as reachable
youknowone Aug 19, 2026
09dca1e
Raise AssertionError where a snippet asserts False
youknowone Aug 19, 2026
0f38af2
Keep raising for a collection that moved under its iterator
youknowone Aug 19, 2026
7392a20
Give apt-get update a deadline to fail on
youknowone Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Stop asking an iterator how long it is to walk it
`PyIter::iter` and `PyIter::into_iter` asked for a length hint and reported
what asking raised. Nothing spent the answer: it reached `PyIterIter` for a
`size_hint()` that every caller either loops past or drops, since collecting
into a `Result` reports no lower bound.

23 operations answered for an iterator whose `__length_hint__` raises, where
they had been raising it: `set`, `frozenset` and the nine `set` methods that
take an iterable, `dict.fromkeys` and the dict view operators, `array` and
`array.extend`, `all`, `any`, `sum`, `io.writelines`, `math.fsum`,
`math.prod`, and `csv.writerow` and `writerows`.

Over a generator, `set()` drops 16% of its instructions and `all()` 23%.

`str.join` and `bytes.join` do ask, reaching their elements through
`PySequence_Fast()`, which fills a list from the iterator. They take
`iter_sized()`, which is now the only way to ask. `iter_without_hint` is gone,
its callers being what `iter` already does.

Assisted-by: Claude
  • Loading branch information
youknowone committed Aug 19, 2026
commit 3613b897c058f5c40ce74525423dd7e537097f1e
4 changes: 2 additions & 2 deletions crates/vm/src/builtins/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ impl PyDict {
}
})?;
elem_iter
.into_iter::<PyObjectRef>(vm)?
.into_iter::<PyObjectRef>(vm)
.collect::<PyResult<Vec<_>>>()
})()
.map_err(|exc| Self::add_update_sequence_note(exc, index, vm))?;
Expand All @@ -257,7 +257,7 @@ impl PyDict {
let iter = seq2.get_iter(vm)?;
let dict = &self.entries;

for (index, element) in iter.iter_without_hint::<PyObjectRef>(vm)?.enumerate() {
for (index, element) in iter.iter::<PyObjectRef>(vm)?.enumerate() {
let (key, value) = Self::update_sequence_pair(element?, index, vm)?;

if !override_existing && dict.contains(vm, &*key)? {
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn iter_search(
) -> PyResult<usize> {
let mut count = 0;
let iter = obj.get_iter(vm)?;
for element in iter.iter_without_hint::<PyObjectRef>(vm)? {
for element in iter.iter::<PyObjectRef>(vm)? {
if vm.bool_eq(item, &*element?)? {
match flag {
SearchType::Index => return Ok(count),
Expand Down
4 changes: 3 additions & 1 deletion crates/vm/src/builtins/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,9 @@ impl PyStr {
iterable: ArgIterable<PyStrRef>,
vm: &VirtualMachine,
) -> PyResult<PyStrRef> {
let iter = iterable.iter(vm)?;
// `PyUnicode_Join()` reaches its elements through `PySequence_Fast()`,
// which fills a list from the iterator and so asks it how long it is.
let iter = iterable.iter_sized(vm)?;
let joined = match iter.exactly_one() {
Ok(first) => {
let first = first?;
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/bytes_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,8 @@ impl PyBytesInner {
}

pub fn join(&self, iterable: ArgIterable<Self>, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
let iter = iterable.iter(vm)?;
// `PySequence_Fast()`, as in `PyUnicode_Join()`.
let iter = iterable.iter_sized(vm)?;
self.elements.py_join(iter)
}

Expand Down
24 changes: 19 additions & 5 deletions crates/vm/src/function/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,30 @@ impl<T> ArgIterable<T> {
&self.iterable
}

/// Returns an iterator over this sequence of objects.
/// This object's iterator.
///
/// This operation may fail if an exception is raised while invoking the
/// `__iter__` method of the iterable object.
pub fn iter<'a>(&self, vm: &'a VirtualMachine) -> PyResult<PyIterIter<'a, T>> {
let iter = PyIter::new(match self.iter_fn {
fn get_iter(&self, vm: &VirtualMachine) -> PyResult<PyIter> {
Ok(PyIter::new(match self.iter_fn {
Some(f) => f(self.iterable.clone(), vm)?,
None => PySequenceIterator::new(self.iterable.clone(), vm)?.into_pyobject(vm),
});
iter.into_iter(vm)
}))
}

/// Returns an iterator over this sequence of objects. See [`PyIter::iter`]
/// for why it does not ask how long the iterator is.
///
/// This operation may fail if an exception is raised while invoking the
/// `__iter__` method of the iterable object.
pub fn iter<'a>(&self, vm: &'a VirtualMachine) -> PyResult<PyIterIter<'a, T>> {
Ok(self.get_iter(vm)?.into_iter(vm))
}

/// [`Self::iter`] for a caller that fills a sized container from the
/// iterator, the way `PySequence_Fast()` does.
pub fn iter_sized<'a>(&self, vm: &'a VirtualMachine) -> PyResult<PyIterIter<'a, T>> {
self.get_iter(vm)?.into_iter_sized(vm)
}
}

Expand Down
26 changes: 16 additions & 10 deletions crates/vm/src/protocol/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,31 @@ where
iternext(self.0.borrow(), vm)
}

/// Walks the iterator without asking it how long it is. Almost nothing
/// asks: a loop over an iterator takes no room up front, so what the
/// object would have answered -- slowly, or by raising -- never runs.
pub fn iter<'a, 'b, U>(
&'b self,
vm: &'a VirtualMachine,
) -> PyResult<PyIterIter<'a, U, &'b PyObject>> {
let length_hint = vm.length_hint_opt(self.as_ref().to_owned())?;
Ok(PyIterIter::new(vm, self.0.borrow(), length_hint))
}

pub fn iter_without_hint<'a, 'b, U>(
&'b self,
vm: &'a VirtualMachine,
) -> PyResult<PyIterIter<'a, U, &'b PyObject>> {
Ok(PyIterIter::new(vm, self.0.borrow(), None))
}
}

impl PyIter<PyObjectRef> {
/// Returns an iterator over this sequence of objects.
pub fn into_iter<U>(self, vm: &VirtualMachine) -> PyResult<PyIterIter<'_, U, PyObjectRef>> {
/// Returns an iterator over this sequence of objects. See [`Self::iter`]
/// for why it does not ask how long the iterator is.
pub fn into_iter<U>(self, vm: &VirtualMachine) -> PyIterIter<'_, U, PyObjectRef> {
PyIterIter::new(vm, self.0, None)
}

/// [`Self::into_iter`] for a caller that fills a sized container from the
/// iterator, the way `PySequence_Fast()` does. It asks how much room that
/// takes and answers with whatever asking raised.
pub fn into_iter_sized<U>(
self,
vm: &VirtualMachine,
) -> PyResult<PyIterIter<'_, U, PyObjectRef>> {
let length_hint = vm.length_hint_opt(self.as_object().to_owned())?;
Ok(PyIterIter::new(vm, self.0, length_hint))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/stdlib/_functools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mod _functools {
iterator,
initial,
} = args;
let mut iter = iterator.iter_without_hint(vm)?;
let mut iter = iterator.iter(vm)?;
// OptionalOption distinguishes between:
// - Missing: no argument provided → use first element from iterator
// - Present(None): explicitly passed None → use None as initial value
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/stdlib/_operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ mod _operator {
#[pyfunction(name = "countOf")]
fn count_of(a: PyIter, b: PyObjectRef, vm: &VirtualMachine) -> PyResult<usize> {
let mut count: usize = 0;
for element in a.iter_without_hint::<PyObjectRef>(vm)? {
for element in a.iter::<PyObjectRef>(vm)? {
let element = element?;
if element.is(&b) || vm.bool_eq(&b, &element)? {
count += 1;
Expand All @@ -199,7 +199,7 @@ mod _operator {

#[pyfunction(name = "indexOf")]
fn index_of(a: PyIter, b: PyObjectRef, vm: &VirtualMachine) -> PyResult<usize> {
for (index, element) in a.iter_without_hint::<PyObjectRef>(vm)?.enumerate() {
for (index, element) in a.iter::<PyObjectRef>(vm)?.enumerate() {
let element = element?;
if element.is(&b) || vm.bool_eq(&b, &element)? {
return Ok(index);
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/stdlib/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2135,7 +2135,7 @@ pub(crate) fn envobj_to_dict(
}
let keys = vm.call_method(obj, "keys", ())?;
let dict = vm.ctx.new_dict();
for key in keys.get_iter(vm)?.into_iter::<PyObjectRef>(vm)? {
for key in keys.get_iter(vm)?.into_iter::<PyObjectRef>(vm) {
let key = key?;
let val = obj.get_item(&*key, vm)?;
dict.set_item(&*key, val, vm)?;
Expand Down
62 changes: 62 additions & 0 deletions extra_tests/snippets/builtin_iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,65 @@ def __len__(self):
assert seq_it.__length_hint__() == 3
next(seq_it)
assert seq_it.__length_hint__() == 2


# Walking an iterator takes no room up front, so nothing on the way asks it how
# long it is. Only join does, reaching its elements through PySequence_Fast(),
# which fills a list from the iterator.
import array
import collections
import io
import math


class LoudIterator:
def __init__(self, seq):
self.i = iter(seq)

def __iter__(self):
return self

def __next__(self):
return next(self.i)

def __length_hint__(self):
raise NotImplementedError("iterator hint")


def handing(seq=(1, 2, 3)):
class Handing:
def __iter__(self):
return LoudIterator(seq)

return Handing()


assert set(handing()) == {1, 2, 3}
assert frozenset(handing()) == frozenset({1, 2, 3})
assert {1}.difference(handing()) == set()
assert {1}.intersection(handing()) == {1}
assert {1}.symmetric_difference(handing()) == {2, 3}
assert {1}.issubset(handing())
assert not {9}.issuperset(handing())
assert dict.fromkeys(handing()) == {1: None, 2: None, 3: None}
assert array.array("b", handing()) == array.array("b", [1, 2, 3])
assert all(handing()) and any(handing())
assert sum(handing()) == 6
assert math.fsum(handing()) == 6.0
assert math.prod(handing()) == 6
assert collections.deque(handing()) == collections.deque([1, 2, 3])
assert tuple(handing()) == (1, 2, 3)
assert list(handing()) == [1, 2, 3]
assert min(handing()) == 1
assert bytes(handing()) == b"\x01\x02\x03"
assert bytearray(handing()) == bytearray(b"\x01\x02\x03")
io.StringIO().writelines(handing(("a", "b")))

# join asks, and answers with what asking raised.
for empty in ("", b""):
try:
empty.join(handing((empty.__class__(),)))
except NotImplementedError:
pass
else:
raise AssertionError(f"{empty.__class__.__name__}.join did not ask")
Loading