| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
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 reviews.auto_review.auto_pause_after_reviewed_commits setting. Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds REPL printing in the compiler; per-frame locals_dirty tracking and locals_to_fast syncing; extends tracing with Call/Return/Line and CCall/CReturn/CException and fires trace events around frames and C-callables; refines generator/coroutine destructor/close behavior and return types; improves OSError/WinError handling; fixes list indexing and tee reentrancy; narrows a syntax-error caret. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem🚥 Pre-merge checks | ✅ 1 | ❌ 2 ❌ Failed checks (1 warning, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. ❤️ ShareComment @coderabbitai help to get the list of available commands and usage tips. |
Sorry, something went wrong.
|
Code has been automatically formatted The code in this PR has been formatted using:
git pull origin doctest |
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)crates/vm/src/builtins/int.rs (1)crates/vm/src/coroutine.rs (1)123-145: ⚠️ Potential issue | 🟠 Major
Fix error messages to match CPython for integer division operations.
CPython 3.12 uses:
- inner_mod: "integer modulo by zero" (not "division by zero")
- inner_floordiv: "integer division or modulo by zero" (not "division by zero")
- inner_divmod: "integer division or modulo by zero" (not "division by zero")
203-228: ⚠️ Potential issue | 🟡 Minor
close() returns the generator's return value instead of None.
CPython's gen_close() discards the generator's return value and always returns None:
Py_DECREF(retval); Py_RETURN_NONE;Here, ExecutionResult::Return(value) on line 226 passes through the generator's actual return value. If a generator has return 42 and the caller does result = gen.close(), CPython yields None but this implementation yields 42.
This is unlikely to matter in practice (the return value of .close() is almost never used), but it's a behavioral divergence from CPython.
Fix to match CPython- Ok(ExecutionResult::Return(value)) => Ok(value), + Ok(ExecutionResult::Return(_)) => Ok(vm.ctx.none()),
In `@crates/codegen/src/compile.rs`:
- Around line 2159-2166: The current interactive printing (guarded by
self.interactive && !self.ctx.in_func() && !self.ctx.in_class) also prints
expression statements nested inside compound blocks; restrict printing to true
top-level only by adding a check that there is no block nesting (e.g., require
ctx.block_depth == 0 or !self.ctx.in_block, or add/use a helper like
ctx.is_module_toplevel()) before emitting Instruction::CallIntrinsic1 { Print },
or alternatively move the echo logic out of the per-statement path into
compile_program_single so only top-level expressions are printed; update the
condition that currently references self.interactive, self.ctx.in_func(), and
self.ctx.in_class accordingly.
In `@crates/vm/src/frame.rs`:
- Around line 261-281: The locals_to_fast method clears locals_dirty before
syncing, so if mapping().subscript returns an Err and the function returns early
the dirty flag is lost; move the locals_dirty.store(false,
atomic::Ordering::Release) to after the for loop completes successfully (just
before returning Ok(())) so the flag is only cleared when all code.varnames have
been processed and fastlocals updated, ensuring a retry on error; update
references in locals_to_fast accordingly (keep use of fastlocals.lock() and the
existing error handling for key_error).
crates/vm/src/vm/vm_new.rs (1)crates/vm/src/builtins/frame.rs (1)555-560: narrow_caret logic is unreachable when python_end_location() returns None.
If the parser ever produces an InvalidStarredExpressionUsage error without an end location, narrow_caret = true would have no effect since this code is gated on if let Some(...). In practice this is likely fine since ruff should always provide end locations for this error type, but worth noting.
crates/vm/src/builtins/coroutine.rs (1)46-49: Minor: locals_dirty is set even when self.locals(vm) fails.
If self.locals(vm) returns an Err, the caller never receives the locals dict, so marking locals_dirty = true is unnecessary. It's harmless (just triggers a no-op sync later), but for correctness:
Suggested fixfn f_locals(&self, vm: &VirtualMachine) -> PyResult { - let result = self.locals(vm).map(Into::into); - self.locals_dirty - .store(true, core::sync::atomic::Ordering::Release); - result + let result = self.locals(vm)?; + self.locals_dirty + .store(true, core::sync::atomic::Ordering::Release); + Ok(result.into()) }crates/vm/src/protocol/callable.rs (1)155-169: Duplicated destructor logic between PyGenerator and PyCoroutine.
The del implementation here is identical to PyGenerator::del in generator.rs (lines 151-168). Consider extracting the common finalization logic into a method on Coro itself (e.g., Coro::finalize(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<()>) and having both destructors delegate to it.
SketchAdd to Coro:
pub fn finalize(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<()> { if self.closed() || self.running() { return Ok(()); } if self.frame.lasti() == 0 { self.closed.store(true); return Ok(()); } if let Err(e) = self.close(jen, vm) { vm.run_unraisable(e, None, jen.to_owned()); } Ok(()) }Then both destructors become:
fn del(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> { zelf.inner.finalize(zelf.as_object(), vm) }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."
55-56: is_python_callable check may miss other Python-level callables.
The check only covers PyFunction and PyBoundMethod. Other Python-level callables like instances with __call__ or class objects won't match and will get c_call/c_return events instead of call/return. This diverges from CPython, where any Python-level __call__ triggers call/return events.
That said, this is likely acceptable for an initial implementation since the main goal is to avoid double-tracing for PyFunction and PyBoundMethod which already emit events via with_frame().
Sorry, something went wrong.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)crates/vm/src/coroutine.rs (1)203-228: ⚠️ Potential issue | 🟡 Minor
close() returns the generator's return value instead of None on Return — deliberate?
CPython's gen_close() unconditionally returns Py_None on success. Line 226 returns the actual ExecutionResult::Return(value), which means if a generator catches GeneratorExit and executes return some_value, close() will surface that value instead of None. This is an extremely rare edge case, but it's a semantic difference from CPython.
If aligning with CPython is desired:
Proposed fix- Ok(ExecutionResult::Return(value)) => Ok(value), + Ok(ExecutionResult::Return(_)) => Ok(vm.ctx.none()),
crates/vm/src/ospath.rs (1)crates/vm/src/stdlib/itertools.rs (1)330-346: Consider extracting shared logic with with_filename.
with_filename_from_errno duplicates with_filename (lines 318–329) almost entirely — the only difference is the #[cfg(windows)] without_winerror() call. You could extract a common helper and pass a flag or closure for the Windows-specific transform, though given how small these methods are, this is not urgent.
♻️ One way to reduce duplication+ fn build_with_filename_inner<'a>( + error: &std::io::Error, + filename: impl Into<OsPathOrFd<'a>>, + strip_winerror: bool, + vm: &VirtualMachine, + ) -> crate::builtins::PyBaseExceptionRef { + use crate::exceptions::ToOSErrorBuilder; + let builder = error.to_os_error_builder(vm); + #[cfg(windows)] + let builder = if strip_winerror { builder.without_winerror() } else { builder }; + let builder = builder.filename(filename.into().filename(vm)); + builder.build(vm).upcast() + } + #[must_use] pub(crate) fn with_filename<'a>( error: &std::io::Error, filename: impl Into<OsPathOrFd<'a>>, vm: &VirtualMachine, ) -> crate::builtins::PyBaseExceptionRef { - use crate::exceptions::ToOSErrorBuilder; - let builder = error.to_os_error_builder(vm); - let builder = builder.filename(filename.into().filename(vm)); - builder.build(vm).upcast() + Self::build_with_filename_inner(error, filename, false, vm) } #[must_use] pub(crate) fn with_filename_from_errno<'a>( error: &std::io::Error, filename: impl Into<OsPathOrFd<'a>>, vm: &VirtualMachine, ) -> crate::builtins::PyBaseExceptionRef { - use crate::exceptions::ToOSErrorBuilder; - let builder = error.to_os_error_builder(vm); - #[cfg(windows)] - let builder = builder.without_winerror(); - let builder = builder.filename(filename.into().filename(vm)); - builder.build(vm).upcast() + Self::build_with_filename_inner(error, filename, true, vm) }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."
crates/vm/src/frame.rs (1)959-983: Reentrancy guard and cache-aware get_item look correct.
The design mirrors CPython's teedataobject pattern well: serve from cache on fast path, guard iterable.next() with an atomic running flag, and append to cache on miss.
One minor observation: on line 982, values[index] relies on the invariant that index is always either < values.len() (served from the fast path on lines 965–967) or == values.len() (pushed on line 980). If this invariant were ever violated (e.g., values.len() < index), it would panic at runtime. Since the tee iterators enforce sequential access, this is safe in practice—same assumption CPython makes—but a debug assertion could catch future regressions:
💡 Optional: add a debug assertionif values.len() == index { values.push(obj); } + debug_assert!( + index < values.len(), + "tee iterator cache invariant violated: index {index} >= len {}", + values.len() + ); Ok(PyIterReturn::Return(values[index].clone()))crates/vm/src/protocol/callable.rs (1)466-479: Locking self.object.trace on every instruction while tracing is active — acceptable but worth noting.
The self.object.trace.lock() inside the hot loop (line 473) is only reached when vm.use_tracing is set, so normal execution is unaffected. However, when a trace function is active, this acquires the mutex on every instruction — even when the line hasn't changed. For a tighter fast path you could cache whether the per-frame trace is set outside the loop, or at least check the line-change condition before acquiring the lock.
That said, this mirrors CPython's approach (check tracing flags, then consult per-frame state), and tracing is inherently slow, so this is fine for now.
52-69: Unconditional to_owned() clone for non-Python callables even when tracing is off.
Line 60 creates an Arc clone of the callable before checking use_tracing inside trace_event. When tracing is disabled (the overwhelmingly common case), this is a wasted atomic increment/decrement on every non-Python call (builtins, C extensions, etc.).
A quick guard avoids the overhead:
Proposed optimization} else { - let callable = self.obj.to_owned(); - vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?; - let result = (self.call)(self.obj, args, vm); - if result.is_ok() { - vm.trace_event(TraceEvent::CReturn, Some(callable))?; - } else { - let _ = vm.trace_event(TraceEvent::CException, Some(callable)); + if vm.use_tracing.get() { + let callable = self.obj.to_owned(); + vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?; + let result = (self.call)(self.obj, args, vm); + if result.is_ok() { + vm.trace_event(TraceEvent::CReturn, Some(callable))?; + } else { + let _ = vm.trace_event(TraceEvent::CException, Some(callable)); + } + result + } else { + (self.call)(self.obj, args, vm) } - result }
Sorry, something went wrong.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)crates/vm/src/coroutine.rs (1)203-228: ⚠️ Potential issue | 🟡 Minor
close() returns the generator's return value instead of always None.
On line 226, Ok(ExecutionResult::Return(value)) => Ok(value) returns whatever the generator produced. In CPython's gen_close (genobject.c), the return value is explicitly discarded and Py_None is always returned. While this may not matter in practice (callers rarely inspect close()'s return value), it's a semantic deviation from CPython.
Consider returning Ok(vm.ctx.none()) for consistency:
Proposed fixOk(ExecutionResult::Yield(_)) => { Err(vm.new_runtime_error(format!("{} ignored GeneratorExit", gen_name(jen, vm)))) } Err(e) if !is_gen_exit(&e, vm) => Err(e), - Ok(ExecutionResult::Return(value)) => Ok(value), + Ok(ExecutionResult::Return(_)) => Ok(vm.ctx.none()), _ => Ok(vm.ctx.none()),
crates/vm/src/stdlib/itertools.rs (1)crates/vm/src/vm/vm_new.rs (1)959-983: Reentrancy guard logic is sound but not panic-safe.
The two-phase approach (cache-first, then fetch-with-guard) is correct. The running flag properly prevents reentrant calls to the underlying iterator, and the values.len() == index check on line 979 correctly handles the case where another thread already populated the cache entry.
However, if self.iterable.next(vm) on line 973 panics (unwind), running remains true permanently, bricking the tee. Consider using a scope guard (e.g., a drop-based RAII wrapper) to ensure running is reset even on panic:
Suggested panic-safe pattern// Prevent concurrent/reentrant calls to iterable.next() if self.running.swap(true, Ordering::Acquire) { return Err(vm.new_runtime_error("cannot re-enter the tee iterator")); } - let result = self.iterable.next(vm); - self.running.store(false, Ordering::Release); + let result = { + struct ResetOnDrop<'a>(&'a AtomicBool); + impl Drop for ResetOnDrop<'_> { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } + } + let _guard = ResetOnDrop(&self.running); + self.iterable.next(vm) + };This is a minor robustness improvement—VM calls generally shouldn't panic, but a drop guard is a cheap safety net.
554-560: Redundant call to error.python_location() — consider reusing the earlier result.
python_location() is already called on line 541 but the result is immediately converted to PyInt, making it unavailable here. A small refactor could store the (u32, u32) tuple before converting, avoiding the duplicate call.
♻️ Suggested refactor- let (lineno, offset) = error.python_location(); - let lineno = self.ctx.new_int(lineno); - let offset = self.ctx.new_int(offset); + let (lineno_raw, offset_raw) = error.python_location(); + let lineno = self.ctx.new_int(lineno_raw); + let offset = self.ctx.new_int(offset_raw);Then at line 555:
if let Some((end_lineno, end_offset)) = error.python_end_location() { let (end_lineno, end_offset) = if narrow_caret { - let (l, o) = error.python_location(); - (l, o + 1) + (lineno_raw, offset_raw + 1) } else { (end_lineno, end_offset) };
Sorry, something went wrong.
- Add interactive REPL mode: auto-print expression results in single mode - Implement Destructor for PyGenerator and PyCoroutine - Add locals_dirty tracking and locals_to_fast() for frame sync - Add per-line tracing with prev_line tracking in execution loop - Fix gen_throw to close sub-iterator on GeneratorExit (gen_close_iter) - Pass callable object as arg in c_call/c_return/c_exception trace events - Distinguish [Errno] vs [WinError] for CRT vs Win32 API errors - Fix tee thread safety with AtomicBool running flag - Fix division error messages to match expected format
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [x] lib: cpython/Lib/doctest.py dependencies:
dependent tests: (29 tests)
[ ] lib: cpython/Lib/pdb.py dependencies:
dependent tests: (no tests depend on pdb) [ ] lib: cpython/Lib/concurrent dependencies:
dependent tests: (11 tests)
[x] lib: cpython/Lib/dbm dependencies:
dependent tests: (4 tests)
[ ] test: cpython/Lib/test/test_extcall.py (TODO: 30) dependencies: dependent tests: (no tests depend on extcall) [ ] test: cpython/Lib/test/test_generators.py (TODO: 12) dependencies: dependent tests: (no tests depend on generator) [x] lib: cpython/Lib/inspect.py dependencies:
dependent tests: (42 tests)
[ ] test: cpython/Lib/test/test_raise.py dependencies: dependent tests: (no tests depend on raise) [ ] test: cpython/Lib/test/test_sys.py (TODO: 15) dependencies: dependent tests: (209 tests)
[ ] lib: cpython/Lib/trace.py dependencies:
dependent tests: (1 tests)
Legend:
|
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)crates/vm/src/builtins/int.rs (2)123-145: ⚠️ Potential issue | 🟠 Major
Error messages diverge from CPython — this may break doctest compatibility.
CPython uses "integer division or modulo by zero" for // and divmod, and "integer modulo by zero" for %. Only true division (/) uses "division by zero". Unifying all three to "division by zero" will cause any doctest that captures the exact ZeroDivisionError message for %, //, or divmod to fail.
The old inner_divmod message reportedly had a typo ("integer division orModulo by zero"), so fixing that is welcome — but the replacement should match CPython.
Proposed fix to match CPython messagesfn inner_mod(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult { if int2.is_zero() { - Err(vm.new_zero_division_error("division by zero")) + Err(vm.new_zero_division_error("integer modulo by zero")) } else { Ok(vm.ctx.new_int(int1.mod_floor(int2)).into()) } } fn inner_floordiv(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult { if int2.is_zero() { - Err(vm.new_zero_division_error("division by zero")) + Err(vm.new_zero_division_error("integer division or modulo by zero")) } else { Ok(vm.ctx.new_int(int1.div_floor(int2)).into()) } } fn inner_divmod(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult { if int2.is_zero() { - return Err(vm.new_zero_division_error("division by zero")); + return Err(vm.new_zero_division_error("integer division or modulo by zero")); }
123-145: ⚠️ Potential issue | 🔴 Critical
Fix error messages for integer zero-division operations to match CPython.
CPython uses distinct error messages for integer operations:
- % (modulo): "integer modulo by zero" or "integer division or modulo by zero"
- // (floor division): "integer division or modulo by zero"
- divmod(): "integer division or modulo by zero"
- / (true division): "division by zero"
The current code incorrectly uses the generic "division by zero" for all three integer operations (inner_mod, inner_floordiv, inner_divmod), which breaks CPython compatibility. This is especially problematic for doctests, which validate exact exception messages. Update these functions to use the correct messages matching CPython's behavior.
In `@crates/vm/src/frame.rs`: - Around line 261-281: locals_to_fast currently leaves fastlocals[i] untouched when locals.mapping().subscript(varname, vm) returns a KeyError, but it should clear the fast slot like CPython's PyFrame_LocalsToFast; update the match in locals_to_fast so that Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) sets fastlocals[i] = None (unbound) instead of doing nothing, ensuring deleted keys in f_locals unbind the corresponding fastlocal; keep the existing Err(e) => return Err(e) path and retain locals_dirty store/release logic. In `@crates/vm/src/protocol/callable.rs`: - Around line 52-69: The code currently always calls self.obj.to_owned() for non-Python callables, bumping refcounts even when tracing is disabled; change the logic to first check the VM tracing flag (e.g., vm.use_tracing) and only call self.obj.to_owned() when tracing is enabled, then call vm.trace_event(TraceEvent::CCall, Some(callable.clone())) before invoking (self.call) and vm.trace_event(TraceEvent::CReturn) / vm.trace_event(TraceEvent::CException) afterwards using that owned `callable`; otherwise, when tracing is disabled, directly invoke (self.call)(self.obj, args, vm) without calling self.obj.to_owned() or any clone to avoid the hot-path refcount bump.
Sorry, something went wrong.
| /// Sync locals dict back to fastlocals. Called before generator/coroutine resume | ||
| /// to apply any modifications made via f_locals. | ||
| pub fn locals_to_fast(&self, vm: &VirtualMachine) -> PyResult<()> { | ||
| if !self.locals_dirty.load(atomic::Ordering::Acquire) { | ||
| return Ok(()); | ||
| } | ||
| let code = &**self.code; | ||
| let mut fastlocals = self.fastlocals.lock(); | ||
| for (i, &varname) in code.varnames.iter().enumerate() { | ||
| if i >= fastlocals.len() { | ||
| break; | ||
| } | ||
| match self.locals.mapping().subscript(varname, vm) { | ||
| Ok(value) => fastlocals[i] = Some(value), | ||
| Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {} | ||
| Err(e) => return Err(e), | ||
| } | ||
| } | ||
| self.locals_dirty.store(false, atomic::Ordering::Release); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
⚠️ Potential issue | 🟡 Minor
locals_to_fast does not clear fastlocals when a key is deleted from f_locals.
When a varname is missing from the locals dict (KeyError on line 275), the corresponding fastlocals[i] slot is left unchanged. In CPython's PyFrame_LocalsToFast, a missing key causes the fast slot to be set to NULL (i.e., the variable becomes unbound). This means del frame.f_locals['x'] followed by a generator resume won't actually unbind x.
If this is intentional (e.g., scoped to what doctest needs), a comment would help. Otherwise:
Proposed fix match self.locals.mapping().subscript(varname, vm) {
Ok(value) => fastlocals[i] = Some(value),
- Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {}
+ Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
+ fastlocals[i] = None;
+ }
Err(e) => return Err(e),
}‼️ 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.
| /// Sync locals dict back to fastlocals. Called before generator/coroutine resume | |
| /// to apply any modifications made via f_locals. | |
| pub fn locals_to_fast(&self, vm: &VirtualMachine) -> PyResult<()> { | |
| if !self.locals_dirty.load(atomic::Ordering::Acquire) { | |
| return Ok(()); | |
| } | |
| let code = &**self.code; | |
| let mut fastlocals = self.fastlocals.lock(); | |
| for (i, &varname) in code.varnames.iter().enumerate() { | |
| if i >= fastlocals.len() { | |
| break; | |
| } | |
| match self.locals.mapping().subscript(varname, vm) { | |
| Ok(value) => fastlocals[i] = Some(value), | |
| Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {} | |
| Err(e) => return Err(e), | |
| } | |
| } | |
| self.locals_dirty.store(false, atomic::Ordering::Release); | |
| Ok(()) | |
| } | |
| /// Sync locals dict back to fastlocals. Called before generator/coroutine resume | |
| /// to apply any modifications made via f_locals. | |
| pub fn locals_to_fast(&self, vm: &VirtualMachine) -> PyResult<()> { | |
| if !self.locals_dirty.load(atomic::Ordering::Acquire) { | |
| return Ok(()); | |
| } | |
| let code = &**self.code; | |
| let mut fastlocals = self.fastlocals.lock(); | |
| for (i, &varname) in code.varnames.iter().enumerate() { | |
| if i >= fastlocals.len() { | |
| break; | |
| } | |
| match self.locals.mapping().subscript(varname, vm) { | |
| Ok(value) => fastlocals[i] = Some(value), | |
| Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => { | |
| fastlocals[i] = None; | |
| } | |
| Err(e) => return Err(e), | |
| } | |
| } | |
| self.locals_dirty.store(false, atomic::Ordering::Release); | |
| Ok(()) | |
| } |
In `@crates/vm/src/frame.rs` around lines 261 - 281, locals_to_fast currently leaves fastlocals[i] untouched when locals.mapping().subscript(varname, vm) returns a KeyError, but it should clear the fast slot like CPython's PyFrame_LocalsToFast; update the match in locals_to_fast so that Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) sets fastlocals[i] = None (unbound) instead of doing nothing, ensuring deleted keys in f_locals unbind the corresponding fastlocal; keep the existing Err(e) => return Err(e) path and retain locals_dirty store/release logic.
Sorry, something went wrong.
| // Python functions get 'call'/'return' events from with_frame(). | ||
| // Bound methods delegate to the inner callable, which fires its own events. | ||
| // All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'. | ||
| let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some() | ||
| || self.obj.downcast_ref::<PyBoundMethod>().is_some(); | ||
| if is_python_callable { | ||
| (self.call)(self.obj, args, vm) | ||
| } else { | ||
| let callable = self.obj.to_owned(); | ||
| vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?; | ||
| let result = (self.call)(self.obj, args, vm); | ||
| if result.is_ok() { | ||
| vm.trace_event(TraceEvent::CReturn, Some(callable))?; | ||
| } else { | ||
| let _ = vm.trace_event(TraceEvent::CException, Some(callable)); | ||
| } | ||
| result | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Unconditional clone of the callable on every non-Python call, even when tracing is off.
self.obj.to_owned() (line 60) bumps the refcount for every built-in call regardless of whether tracing is active. Since vm.use_tracing is usually false, gate the tracing path behind the check:
Proposed fix if is_python_callable {
(self.call)(self.obj, args, vm)
+ } else if vm.use_tracing.get() {
+ let callable = self.obj.to_owned();
+ vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
+ let result = (self.call)(self.obj, args, vm);
+ if result.is_ok() {
+ vm.trace_event(TraceEvent::CReturn, Some(callable))?;
+ } else {
+ let _ = vm.trace_event(TraceEvent::CException, Some(callable));
+ }
+ result
} else {
- let callable = self.obj.to_owned();
- vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
- let result = (self.call)(self.obj, args, vm);
- if result.is_ok() {
- vm.trace_event(TraceEvent::CReturn, Some(callable))?;
- } else {
- let _ = vm.trace_event(TraceEvent::CException, Some(callable));
- }
- result
+ (self.call)(self.obj, args, vm)
}This avoids the to_owned() + clone() on the hot path when no tracer is registered.
📝 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.
| // Python functions get 'call'/'return' events from with_frame(). | |
| // Bound methods delegate to the inner callable, which fires its own events. | |
| // All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'. | |
| let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some() | |
| || self.obj.downcast_ref::<PyBoundMethod>().is_some(); | |
| if is_python_callable { | |
| (self.call)(self.obj, args, vm) | |
| } else { | |
| let callable = self.obj.to_owned(); | |
| vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?; | |
| let result = (self.call)(self.obj, args, vm); | |
| if result.is_ok() { | |
| vm.trace_event(TraceEvent::CReturn, Some(callable))?; | |
| } else { | |
| let _ = vm.trace_event(TraceEvent::CException, Some(callable)); | |
| } | |
| result | |
| } | |
| // Python functions get 'call'/'return' events from with_frame(). | |
| // Bound methods delegate to the inner callable, which fires its own events. | |
| // All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'. | |
| let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some() | |
| || self.obj.downcast_ref::<PyBoundMethod>().is_some(); | |
| if is_python_callable { | |
| (self.call)(self.obj, args, vm) | |
| } else if vm.use_tracing.get() { | |
| let callable = self.obj.to_owned(); | |
| vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?; | |
| let result = (self.call)(self.obj, args, vm); | |
| if result.is_ok() { | |
| vm.trace_event(TraceEvent::CReturn, Some(callable))?; | |
| } else { | |
| let _ = vm.trace_event(TraceEvent::CException, Some(callable)); | |
| } | |
| result | |
| } else { | |
| (self.call)(self.obj, args, vm) | |
| } |
In `@crates/vm/src/protocol/callable.rs` around lines 52 - 69, The code currently always calls self.obj.to_owned() for non-Python callables, bumping refcounts even when tracing is disabled; change the logic to first check the VM tracing flag (e.g., vm.use_tracing) and only call self.obj.to_owned() when tracing is enabled, then call vm.trace_event(TraceEvent::CCall, Some(callable.clone())) before invoking (self.call) and vm.trace_event(TraceEvent::CReturn) / vm.trace_event(TraceEvent::CException) afterwards using that owned `callable`; otherwise, when tracing is disabled, directly invoke (self.call)(self.obj, args, vm) without calling self.obj.to_owned() or any clone to avoid the hot-path refcount bump.
Sorry, something went wrong.
Update doctest,test_generators from v3.14.2 and fix generator bugs
| Back | FazBrowse Home | New Git URL |
Summary by CodeRabbit
New Features
Bug Fixes
Performance & Stability