Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughIntroduces a fast path in Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 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
tupleandlistvalues are handled by the fast path above (lines 3022-3034) and never reach this code. Onlydict_typecan 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.
clsis a reference (no destructor), and it was already consumed by theif/else ifchecks 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`.
There was a problem hiding this comment.
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.
| use std::{ | ||
| env::{self, VarError}, | ||
| io::{IsTerminal, Read, Write}, | ||
| io::{IsTerminal, Write}, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /root/repo && rg -n "read_to_string" crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 125
🏁 Script executed:
cd /root/repo && rg -n "use std::io::Read" crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 125
🏁 Script executed:
cd /root/repo && rg -n "Read" crates/vm/src/stdlib/sys.rs -A 2 -B 2Repository: RustPython/RustPython
Length of output: 125
🏁 Script executed:
cd /root/repo && head -80 crates/vm/src/stdlib/sys.rs | tail -30Repository: RustPython/RustPython
Length of output: 125
🏁 Script executed:
rg -n "read_to_string" crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 109
🏁 Script executed:
rg -n "use std::io::Read" crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
rg -n "_baserepl" crates/vm/src/stdlib/sys.rs -A 5 -B 2Repository: 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 -iRepository: RustPython/RustPython
Length of output: 128
🏁 Script executed:
head -60 crates/vm/src/stdlib/sys.rs | tail -20Repository: RustPython/RustPython
Length of output: 605
🏁 Script executed:
rg -n "Read" crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
wc -l crates/vm/src/stdlib/sys.rsRepository: RustPython/RustPython
Length of output: 97
🏁 Script executed:
rg -n "^mod\|^pub mod\|^fn\|^pub fn\|#\[cfg" crates/vm/src/stdlib/sys.rs | head -40Repository: 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 -20Repository: 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.
| 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.
|
Code has been automatically formatted The code in this PR has been formatted using:
git pull origin unpack |
There was a problem hiding this comment.
🧹 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 clonedVecis never used. You could compare the length first (viaas_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 aVecat 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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)
3084-3111: Dict-specific branch in the exhaustion check: thegot <= sizefallback is unreachable for non-mutated dicts.On line 3092,
if got > sizeshould always be true for a non-concurrently-mutated exact dict (we already pulledsizeitems and got one more), making theelseon 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 includegotin the message whenvalue.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.
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_unpack.py (TODO: 1) dependencies: dependent tests: (no tests depend on unpack) Legend:
|
Summary by CodeRabbit
Bug Fixes
Performance