Skip to content

Commit 99d74e9

Browse files
committed
Fix unpack_sequence to match CPython behavior
Add fast path for exact tuple/list (direct length check without iterator). General path now iterates only size+1 elements, fixing hang on infinite sequences like __getitem__ without raising IndexError. Error messages now include "got N" for tuple, list, and dict. Remove 7 expectedFailure/skip markers from test_unpack.
1 parent b87386f commit 99d74e9

File tree

2 files changed

+87
-22
lines changed

2 files changed

+87
-22
lines changed

Lib/test/test_unpack.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,14 @@
7070
7171
Unpacking tuple of wrong size
7272
73-
>>> a, b = t # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
73+
>>> a, b = t
7474
Traceback (most recent call last):
7575
...
7676
ValueError: too many values to unpack (expected 2, got 3)
7777
7878
Unpacking tuple of wrong size
7979
80-
>>> a, b = l # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
80+
>>> a, b = l
8181
Traceback (most recent call last):
8282
...
8383
ValueError: too many values to unpack (expected 2, got 3)
@@ -144,7 +144,7 @@
144144
145145
Unpacking to an empty iterable should raise ValueError
146146
147-
>>> () = [42] # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
147+
>>> () = [42]
148148
Traceback (most recent call last):
149149
...
150150
ValueError: too many values to unpack (expected 0, got 1)
@@ -157,13 +157,13 @@
157157
Traceback (most recent call last):
158158
...
159159
ValueError: too many values to unpack (expected 3)
160-
>>> next(it) # TODO: RUSTPYTHON; Raise `StopIteration` # doctest: +SKIP
160+
>>> next(it)
161161
4
162162
163163
Unpacking unbalanced dict
164164
165165
>>> d = {4: 'four', 5: 'five', 6: 'six', 7: 'seven'}
166-
>>> a, b, c = d # TODO: RUSTPYTHON; # doctest: +EXPECTED_FAILURE
166+
>>> a, b, c = d
167167
Traceback (most recent call last):
168168
...
169169
ValueError: too many values to unpack (expected 3, got 4)
@@ -176,7 +176,7 @@
176176
... def __getitem__(self, i):
177177
... return i*2
178178
...
179-
>>> x, y, z = LengthTooLong() # TODO: RUSTPYTHON; Hangs # doctest: +SKIP
179+
>>> x, y, z = LengthTooLong()
180180
Traceback (most recent call last):
181181
...
182182
ValueError: too many values to unpack (expected 3)
@@ -189,7 +189,7 @@
189189
... def __getitem__(self, i):
190190
... return i*2
191191
...
192-
>>> x, y, z = BadLength() # TODO: RUSTPYTHON; Hangs # doctest: +SKIP
192+
>>> x, y, z = BadLength()
193193
Traceback (most recent call last):
194194
...
195195
ValueError: too many values to unpack (expected 3)

crates/vm/src/frame.rs

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3010,13 +3010,54 @@ impl ExecutingFrame<'_> {
30103010
Ok(None)
30113011
}
30123012

3013+
/// _PyEval_UnpackIterableStackRef
30133014
fn unpack_sequence(&mut self, size: u32, vm: &VirtualMachine) -> FrameResult {
30143015
let value = self.pop_value();
3016+
let size = size as usize;
3017+
3018+
// Fast path for exact tuple/list types (not subclasses) — check
3019+
// length directly without creating an iterator, matching
3020+
// UNPACK_SEQUENCE_TUPLE / UNPACK_SEQUENCE_LIST specializations.
3021+
let cls = value.class();
3022+
let fast_elements: Option<Vec<PyObjectRef>> = if cls.is(vm.ctx.types.tuple_type) {
3023+
Some(value.downcast_ref::<PyTuple>().unwrap().as_slice().to_vec())
3024+
} else if cls.is(vm.ctx.types.list_type) {
3025+
Some(
3026+
value
3027+
.downcast_ref::<PyList>()
3028+
.unwrap()
3029+
.borrow_vec()
3030+
.to_vec(),
3031+
)
3032+
} else {
3033+
None
3034+
};
3035+
if let Some(elements) = fast_elements {
3036+
return match elements.len().cmp(&size) {
3037+
core::cmp::Ordering::Equal => {
3038+
self.state
3039+
.stack
3040+
.extend(elements.into_iter().rev().map(Some));
3041+
Ok(None)
3042+
}
3043+
core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
3044+
"too many values to unpack (expected {size}, got {})",
3045+
elements.len()
3046+
))),
3047+
core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
3048+
"not enough values to unpack (expected {size}, got {})",
3049+
elements.len()
3050+
))),
3051+
};
3052+
}
3053+
3054+
// General path — iterate up to `size + 1` elements to avoid
3055+
// consuming the entire iterator (fixes hang on infinite sequences).
30153056
let not_iterable = value.class().slots.iter.load().is_none()
30163057
&& value
30173058
.get_class_attr(vm.ctx.intern_str("__getitem__"))
30183059
.is_none();
3019-
let elements: Vec<_> = value.try_to_value(vm).map_err(|e| {
3060+
let iter = PyIter::try_from_object(vm, value.clone()).map_err(|e| {
30203061
if not_iterable && e.class().is(vm.ctx.exceptions.type_error) {
30213062
vm.new_type_error(format!(
30223063
"cannot unpack non-iterable {} object",
@@ -3026,24 +3067,48 @@ impl ExecutingFrame<'_> {
30263067
e
30273068
}
30283069
})?;
3029-
let msg = match elements.len().cmp(&(size as usize)) {
3030-
core::cmp::Ordering::Equal => {
3031-
// Wrap each element in Some() for Option<PyObjectRef> stack
3070+
3071+
let mut elements = Vec::with_capacity(size);
3072+
for _ in 0..size {
3073+
match iter.next(vm)? {
3074+
PyIterReturn::Return(item) => elements.push(item),
3075+
PyIterReturn::StopIteration(_) => {
3076+
return Err(vm.new_value_error(format!(
3077+
"not enough values to unpack (expected {size}, got {})",
3078+
elements.len()
3079+
)));
3080+
}
3081+
}
3082+
}
3083+
3084+
// Check that the iterator is exhausted.
3085+
match iter.next(vm)? {
3086+
PyIterReturn::Return(_) => {
3087+
// For exact dict types, show "got N" using the container's
3088+
// size (PyDict_Size). Exact tuple/list are handled by the
3089+
// fast path above and never reach here.
3090+
let msg = if value.class().is(vm.ctx.types.dict_type) {
3091+
if let Ok(got) = value.length(vm) {
3092+
if got > size {
3093+
format!("too many values to unpack (expected {size}, got {got})")
3094+
} else {
3095+
format!("too many values to unpack (expected {size})")
3096+
}
3097+
} else {
3098+
format!("too many values to unpack (expected {size})")
3099+
}
3100+
} else {
3101+
format!("too many values to unpack (expected {size})")
3102+
};
3103+
Err(vm.new_value_error(msg))
3104+
}
3105+
PyIterReturn::StopIteration(_) => {
30323106
self.state
30333107
.stack
30343108
.extend(elements.into_iter().rev().map(Some));
3035-
return Ok(None);
3036-
}
3037-
core::cmp::Ordering::Greater => {
3038-
format!("too many values to unpack (expected {size})")
3109+
Ok(None)
30393110
}
3040-
core::cmp::Ordering::Less => format!(
3041-
"not enough values to unpack (expected {}, got {})",
3042-
size,
3043-
elements.len()
3044-
),
3045-
};
3046-
Err(vm.new_value_error(msg))
3111+
}
30473112
}
30483113

30493114
fn convert_value(

0 commit comments

Comments
 (0)