Skip to content

Fix unpack msg#7175

Merged
youknowone merged 1 commit intoRustPython:mainfrom
youknowone:unpack
Feb 17, 2026
Merged

Fix unpack msg#7175
youknowone merged 1 commit intoRustPython:mainfrom
youknowone:unpack

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Feb 16, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved and clarified error messages for various sequence-unpacking failures, ensuring messages match Python semantics (including dicts and non-iterables).
    • Strengthened validation when unpacking to correctly handle too few or too many values.
  • Performance

    • Faster unpacking for exact tuple and list objects by using a direct extraction path, reducing overhead for common cases.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a fast path in unpack_sequence to directly extract elements from exact tuple/list objects, refactors the general iterator-based path to enforce exact element counts, and revises error messages (including dict-specific wording) to align with Python semantics.

Changes

Cohort / File(s) Summary
Sequence Unpacking Optimization
crates/vm/src/frame.rs
Adds a fast path for exact tuple/list unpacking that avoids iterator creation and directly reads elements; refactors the fallback to use a PyIter collecting exactly size items; adds post-iteration exhaustion checks and improved, Python-aligned error messages (special-casing dicts and non-iterables).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Quick paws, quick mind, I hop and see,
Tuples yield their treasures straight to me,
No needless iterators in my trail,
Errors whisper clearly, short and hale,
I unpack with joy — efficient and free!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix unpack msg' is vague and does not clearly convey the specific nature of the changes, which involve optimizing unpack_sequence with fast paths and improving error messages. Consider using a more descriptive title such as 'Optimize unpack_sequence with fast paths and improve error messages' to better reflect the actual changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@youknowone youknowone marked this pull request as ready for review February 17, 2026 00:40
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/vm/src/frame.rs (2)

3086-3116: Nit: tuple/list type checks on Lines 3092-3093 are unreachable in the general path.

Exact tuple and list values are handled by the fast path above (lines 3022-3034) and never reach this code. Only dict_type can match here. The dead checks are harmless but slightly misleading.

Simplify to only check dict_type
                 let cls = value.class();
-                let msg = if cls.is(vm.ctx.types.list_type)
-                    || cls.is(vm.ctx.types.tuple_type)
-                    || cls.is(vm.ctx.types.dict_type)
-                {
+                let msg = if cls.is(vm.ctx.types.dict_type) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 3086 - 3116, The branch handling
PyIterReturn::Return contains unreachable checks for list/tuple types because
those are handled earlier in the fast path; update the logic in the return
branch by removing the list_type and tuple_type checks and only test
cls.is(vm.ctx.types.dict_type) when deciding whether to attempt value.length(vm)
and include "got N" in the message; ensure you keep the same fallback message
generation (format!("too many values to unpack (expected {size})") and
vm.new_value_error(msg)) so behavior for non-dict iterables remains unchanged
(refer to iter.next(vm), PyIterReturn::Return, value.class(), cls.is(...),
value.length(vm), size, and vm.new_value_error).

3035-3035: Nit: let _ = cls; is a no-op.

cls is a reference (no destructor), and it was already consumed by the if/else if checks above, so this line has no effect — it neither drops a resource nor suppresses an unused-variable warning.

Proposed fix
-        let _ = cls;
-
-        if let Some(elements) = fast_elements {
+        if let Some(elements) = fast_elements {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` at line 3035, The statement `let _ = cls;` in
frame.rs is a no-op because `cls` is a reference already consumed by the
preceding `if`/`else if` branches; remove that line to avoid misleading/no-op
code. If the intent was to silence an unused-variable warning, instead prefix
the binding with an underscore where `cls` is declared or explicitly call
`drop(cls)` (if ownership needs to be released); otherwise simply delete `let _
= cls;` near the end of the matching `if`/`else if` block that references `cls`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 3086-3116: The branch handling PyIterReturn::Return contains
unreachable checks for list/tuple types because those are handled earlier in the
fast path; update the logic in the return branch by removing the list_type and
tuple_type checks and only test cls.is(vm.ctx.types.dict_type) when deciding
whether to attempt value.length(vm) and include "got N" in the message; ensure
you keep the same fallback message generation (format!("too many values to
unpack (expected {size})") and vm.new_value_error(msg)) so behavior for non-dict
iterables remains unchanged (refer to iter.next(vm), PyIterReturn::Return,
value.class(), cls.is(...), value.length(vm), size, and vm.new_value_error).
- Line 3035: The statement `let _ = cls;` in frame.rs is a no-op because `cls`
is a reference already consumed by the preceding `if`/`else if` branches; remove
that line to avoid misleading/no-op code. If the intent was to silence an
unused-variable warning, instead prefix the binding with an underscore where
`cls` is declared or explicitly call `drop(cls)` (if ownership needs to be
released); otherwise simply delete `let _ = cls;` near the end of the matching
`if`/`else if` block that references `cls`.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

3018-3051: Avoid cloning full tuple/list when lengths mismatch.
The fast path currently clones the entire container just to compare lengths; for large mismatches this defeats the optimization. Consider checking length first and only cloning/pushing when equal.

♻️ Suggested refactor
-        let cls = value.class();
-        let fast_elements: Option<Vec<PyObjectRef>> = if cls.is(vm.ctx.types.tuple_type) {
-            Some(value.downcast_ref::<PyTuple>().unwrap().as_slice().to_vec())
-        } else if cls.is(vm.ctx.types.list_type) {
-            Some(
-                value
-                    .downcast_ref::<PyList>()
-                    .unwrap()
-                    .borrow_vec()
-                    .to_vec(),
-            )
-        } else {
-            None
-        };
-        if let Some(elements) = fast_elements {
-            return match elements.len().cmp(&size) {
+        let cls = value.class();
+        if cls.is(vm.ctx.types.tuple_type) {
+            let tuple = value.downcast_ref::<PyTuple>().unwrap();
+            return match tuple.len().cmp(&size) {
                 core::cmp::Ordering::Equal => {
                     self.state
                         .stack
-                        .extend(elements.into_iter().rev().map(Some));
+                        .extend(tuple.as_slice().iter().rev().cloned().map(Some));
                     Ok(None)
                 }
                 core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
                     "too many values to unpack (expected {size}, got {})",
-                    elements.len()
+                    tuple.len()
                 ))),
                 core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
                     "not enough values to unpack (expected {size}, got {})",
-                    elements.len()
+                    tuple.len()
                 ))),
             };
-        }
+        } else if cls.is(vm.ctx.types.list_type) {
+            let list = value.downcast_ref::<PyList>().unwrap();
+            let elements = list.borrow_vec();
+            return match elements.len().cmp(&size) {
+                core::cmp::Ordering::Equal => {
+                    self.state
+                        .stack
+                        .extend(elements.iter().rev().cloned().map(Some));
+                    Ok(None)
+                }
+                core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
+                    "too many values to unpack (expected {size}, got {})",
+                    elements.len()
+                ))),
+                core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
+                    "not enough values to unpack (expected {size}, got {})",
+                    elements.len()
+                ))),
+            };
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 3018 - 3051, The fast-path in the unpack
sequence logic (the cls/fast_elements branch) currently clones the whole
tuple/list via to_vec()/to_vec() before comparing lengths; change it to first
check length from the concrete type (e.g., call PyTuple::len or
PyList::borrow_vec().len or an equivalent length accessor on value.class checks)
and only allocate/clone the elements when the length equals size, then push them
onto self.state.stack (preserving the existing reverse+Some mapping). Update the
branch that handles Ordering::Equal to perform the cloning/extend, and make the
Greater/Less arms return the error without allocating.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/stdlib/sys.rs`:
- Around line 53-56: The build fails when the rustpython-compiler feature is
enabled because _baserepl calls read_to_string but std::io::Read is not
imported; add a cfg-gated import for the Read trait (e.g., #[cfg(feature =
"rustpython-compiler")] use std::io::Read;) alongside the existing io imports in
sys.rs so Read is in scope for the _baserepl function without causing
unused-import warnings when the feature is disabled.

---

Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 3018-3051: The fast-path in the unpack sequence logic (the
cls/fast_elements branch) currently clones the whole tuple/list via
to_vec()/to_vec() before comparing lengths; change it to first check length from
the concrete type (e.g., call PyTuple::len or PyList::borrow_vec().len or an
equivalent length accessor on value.class checks) and only allocate/clone the
elements when the length equals size, then push them onto self.state.stack
(preserving the existing reverse+Some mapping). Update the branch that handles
Ordering::Equal to perform the cloning/extend, and make the Greater/Less arms
return the error without allocating.

Comment on lines 53 to 56
use std::{
env::{self, VarError},
io::{IsTerminal, Read, Write},
io::{IsTerminal, Write},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /root/repo && rg -n "read_to_string" crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 125


🏁 Script executed:

cd /root/repo && rg -n "use std::io::Read" crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 125


🏁 Script executed:

cd /root/repo && rg -n "Read" crates/vm/src/stdlib/sys.rs -A 2 -B 2

Repository: RustPython/RustPython

Length of output: 125


🏁 Script executed:

cd /root/repo && head -80 crates/vm/src/stdlib/sys.rs | tail -30

Repository: RustPython/RustPython

Length of output: 125


🏁 Script executed:

rg -n "read_to_string" crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 109


🏁 Script executed:

rg -n "use std::io::Read" crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

rg -n "_baserepl" crates/vm/src/stdlib/sys.rs -A 5 -B 2

Repository: RustPython/RustPython

Length of output: 375


🏁 Script executed:

cat -n crates/vm/src/stdlib/sys.rs | sed -n '40,80p'

Repository: RustPython/RustPython

Length of output: 1639


🏁 Script executed:

cat -n crates/vm/src/stdlib/sys.rs | sed -n '759,770p'

Repository: RustPython/RustPython

Length of output: 716


🏁 Script executed:

rg -n "use.*Read" crates/vm/src/stdlib/sys.rs -i

Repository: RustPython/RustPython

Length of output: 128


🏁 Script executed:

head -60 crates/vm/src/stdlib/sys.rs | tail -20

Repository: RustPython/RustPython

Length of output: 605


🏁 Script executed:

rg -n "Read" crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

wc -l crates/vm/src/stdlib/sys.rs

Repository: RustPython/RustPython

Length of output: 97


🏁 Script executed:

rg -n "^mod\|^pub mod\|^fn\|^pub fn\|#\[cfg" crates/vm/src/stdlib/sys.rs | head -40

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

cat -n crates/vm/src/stdlib/sys.rs | sed -n '1,60p'

Repository: RustPython/RustPython

Length of output: 2295


🏁 Script executed:

rg -n "#\[cfg" crates/vm/src/stdlib/sys.rs | head -20

Repository: RustPython/RustPython

Length of output: 862


🏁 Script executed:

cat -n crates/vm/src/stdlib/sys.rs | sed -n '51,58p'

Repository: RustPython/RustPython

Length of output: 298


Missing Read trait import breaks builds with rustpython-compiler feature enabled.

The _baserepl function on line 767 calls read_to_string, which requires the std::io::Read trait to be in scope. It is not imported in the current code (lines 53–56), causing a compiler error. Add a cfg-gated import to avoid unused warnings when the feature is disabled:

🔧 Fix
 use std::{
     env::{self, VarError},
     io::{IsTerminal, Write},
 };
+
+#[cfg(feature = "rustpython-compiler")]
+use std::io::Read;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use std::{
env::{self, VarError},
io::{IsTerminal, Read, Write},
io::{IsTerminal, Write},
};
use std::{
env::{self, VarError},
io::{IsTerminal, Write},
};
#[cfg(feature = "rustpython-compiler")]
use std::io::Read;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/stdlib/sys.rs` around lines 53 - 56, The build fails when the
rustpython-compiler feature is enabled because _baserepl calls read_to_string
but std::io::Read is not imported; add a cfg-gated import for the Read trait
(e.g., #[cfg(feature = "rustpython-compiler")] use std::io::Read;) alongside the
existing io imports in sys.rs so Read is in scope for the _baserepl function
without causing unused-import warnings when the feature is disabled.

@github-actions
Copy link
Contributor

Code has been automatically formatted

The code in this PR has been formatted using:

  • cargo fmt --all
    Please pull the latest changes before pushing again:
git pull origin unpack

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/vm/src/frame.rs (2)

3022-3051: Consider checking length before cloning elements to avoid unnecessary allocations on error paths.

When the length doesn't match size, the cloned Vec is never used. You could compare the length first (via as_slice().len() / borrow_vec().len()) and only clone when equal, avoiding allocation + clone overhead for the mismatch cases.

♻️ Sketch
-        let fast_elements: Option<Vec<PyObjectRef>> = if cls.is(vm.ctx.types.tuple_type) {
-            Some(value.downcast_ref::<PyTuple>().unwrap().as_slice().to_vec())
-        } else if cls.is(vm.ctx.types.list_type) {
-            Some(
-                value
-                    .downcast_ref::<PyList>()
-                    .unwrap()
-                    .borrow_vec()
-                    .to_vec(),
-            )
-        } else {
-            None
-        };
-        if let Some(elements) = fast_elements {
-            return match elements.len().cmp(&size) {
-                core::cmp::Ordering::Equal => {
-                    self.state
-                        .stack
-                        .extend(elements.into_iter().rev().map(Some));
-                    Ok(None)
-                }
-                core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
-                    "too many values to unpack (expected {size}, got {})",
-                    elements.len()
-                ))),
-                core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
-                    "not enough values to unpack (expected {size}, got {})",
-                    elements.len()
-                ))),
-            };
-        }
+        if cls.is(vm.ctx.types.tuple_type) {
+            let slice = value.downcast_ref::<PyTuple>().unwrap().as_slice();
+            let got = slice.len();
+            return match got.cmp(&size) {
+                core::cmp::Ordering::Equal => {
+                    self.state.stack.extend(slice.iter().rev().cloned().map(Some));
+                    Ok(None)
+                }
+                core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
+                    "too many values to unpack (expected {size}, got {got})"
+                ))),
+                core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
+                    "not enough values to unpack (expected {size}, got {got})"
+                ))),
+            };
+        } else if cls.is(vm.ctx.types.list_type) {
+            let borrowed = value.downcast_ref::<PyList>().unwrap().borrow_vec();
+            let got = borrowed.len();
+            return match got.cmp(&size) {
+                core::cmp::Ordering::Equal => {
+                    self.state.stack.extend(borrowed.iter().rev().cloned().map(Some));
+                    Ok(None)
+                }
+                core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
+                    "too many values to unpack (expected {size}, got {got})"
+                ))),
+                core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
+                    "not enough values to unpack (expected {size}, got {got})"
+                ))),
+            };
+        }

This also avoids the intermediate Option<Vec<_>> and the extra indirection. For tuples, you can iterate the slice directly without allocating a Vec at all.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 3022 - 3051, The current code eagerly
clones tuples/lists into a Vec in the `fast_elements` branch even when lengths
don't match; instead first check the sequence length (for tuple use
`as_slice().len()`, for list use `borrow_vec().len()`), compare it to `size`,
return the appropriate `Vm` error when lengths differ, and only then clone or
iterate elements when lengths are equal; replace the `Option<Vec<_>>` pattern by
branching on `cls.is(...)` to obtain the length, and when equal either iterate
the tuple slice (no allocation) or clone the list via `to_vec()` and then call
`self.state.stack.extend(... .rev().map(Some))` as before.

3086-3103: Simplify nested branches — three of four produce the same message.

As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code."

♻️ Proposed simplification
-                let msg = if value.class().is(vm.ctx.types.dict_type) {
-                    if let Ok(got) = value.length(vm) {
-                        if got > size {
-                            format!("too many values to unpack (expected {size}, got {got})")
-                        } else {
-                            format!("too many values to unpack (expected {size})")
-                        }
-                    } else {
-                        format!("too many values to unpack (expected {size})")
-                    }
-                } else {
-                    format!("too many values to unpack (expected {size})")
-                };
+                let got = if value.class().is(vm.ctx.types.dict_type) {
+                    value.length(vm).ok().filter(|&n| n > size)
+                } else {
+                    None
+                };
+                let msg = if let Some(got) = got {
+                    format!("too many values to unpack (expected {size}, got {got})")
+                } else {
+                    format!("too many values to unpack (expected {size})")
+                };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 3086 - 3103, The nested branches in the
PyIterReturn::Return arm duplicate the same error text; refactor by first
computing an Option<usize> named e.g. got_opt using
value.class().is(vm.ctx.types.dict_type) then value.length(vm) (map success to
Some(got) else None), and then build the msg once: if let Some(got) = got_opt
and got > size => "too many values...got {got}", else "too many
values...expected {size}" (or the variant without got). Finally return
Err(vm.new_value_error(msg)). Update the code in the PyIterReturn::Return block
to use the common message construction instead of the four nested format
branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 3022-3051: The current code eagerly clones tuples/lists into a Vec
in the `fast_elements` branch even when lengths don't match; instead first check
the sequence length (for tuple use `as_slice().len()`, for list use
`borrow_vec().len()`), compare it to `size`, return the appropriate `Vm` error
when lengths differ, and only then clone or iterate elements when lengths are
equal; replace the `Option<Vec<_>>` pattern by branching on `cls.is(...)` to
obtain the length, and when equal either iterate the tuple slice (no allocation)
or clone the list via `to_vec()` and then call `self.state.stack.extend(...
.rev().map(Some))` as before.
- Around line 3086-3103: The nested branches in the PyIterReturn::Return arm
duplicate the same error text; refactor by first computing an Option<usize>
named e.g. got_opt using value.class().is(vm.ctx.types.dict_type) then
value.length(vm) (map success to Some(got) else None), and then build the msg
once: if let Some(got) = got_opt and got > size => "too many values...got
{got}", else "too many values...expected {size}" (or the variant without got).
Finally return Err(vm.new_value_error(msg)). Update the code in the
PyIterReturn::Return block to use the common message construction instead of the
four nested format branches.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

3084-3111: Dict-specific branch in the exhaustion check: the got <= size fallback is unreachable for non-mutated dicts.

On line 3092, if got > size should always be true for a non-concurrently-mutated exact dict (we already pulled size items and got one more), making the else on line 3094 a dead branch under normal conditions. This is acceptable as defensive coding against concurrent dict mutation during iteration, but consider simplifying to always include got in the message when value.length(vm) succeeds, which is what CPython does:

Suggested simplification
                 let msg = if value.class().is(vm.ctx.types.dict_type) {
                     if let Ok(got) = value.length(vm) {
-                        if got > size {
-                            format!("too many values to unpack (expected {size}, got {got})")
-                        } else {
-                            format!("too many values to unpack (expected {size})")
-                        }
+                        format!("too many values to unpack (expected {size}, got {got})")
                     } else {
                         format!("too many values to unpack (expected {size})")
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 3084 - 3111, The dict-specific branch
builds an error message that omits the actual dict size when value.length(vm)
succeeds, creating an unreachable else for non-mutated dicts; update the code in
the exhaustion check (the match handling PyIterReturn::Return in frame.rs around
iter.next(vm)) so that when value.class().is(vm.ctx.types.dict_type) and
value.length(vm) returns Ok(got) you always include "got {got}" in the message
(i.e., format!("too many values to unpack (expected {size}, got {got})")), and
only fall back to the shorter message when value.length(vm) returns an Err,
leaving the other branches unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 3084-3111: The dict-specific branch builds an error message that
omits the actual dict size when value.length(vm) succeeds, creating an
unreachable else for non-mutated dicts; update the code in the exhaustion check
(the match handling PyIterReturn::Return in frame.rs around iter.next(vm)) so
that when value.class().is(vm.ctx.types.dict_type) and value.length(vm) returns
Ok(got) you always include "got {got}" in the message (i.e., format!("too many
values to unpack (expected {size}, got {got})")), and only fall back to the
shorter message when value.length(vm) returns an Err, leaving the other branches
unchanged.

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.
@github-actions
Copy link
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] test: cpython/Lib/test/test_unpack.py (TODO: 1)
[ ] test: cpython/Lib/test/test_unpack_ex.py (TODO: 11)

dependencies:

dependent tests: (no tests depend on unpack)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone youknowone merged commit d4d010e into RustPython:main Feb 17, 2026
14 checks passed
@youknowone youknowone deleted the unpack branch February 17, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant