X Tutup
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 17 additions & 14 deletions crates/stdlib/src/faulthandler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,8 @@ mod decl {
all_threads: AtomicBool::new(true),
};

/// Arc<Mutex<Vec<FrameRef>>> - shared frame slot for a thread
#[cfg(feature = "threading")]
type ThreadFrameSlot = Arc<parking_lot::Mutex<Vec<crate::vm::frame::FrameRef>>>;
type ThreadFrameSlot = Arc<rustpython_vm::vm::thread::ThreadSlot>;

// Watchdog thread state for dump_traceback_later
struct WatchdogState {
Expand Down Expand Up @@ -326,7 +325,7 @@ mod decl {

/// Write a frame's info to an fd using signal-safe I/O.
#[cfg(any(unix, windows))]
fn dump_frame_from_ref(fd: i32, frame: &crate::vm::PyRef<Frame>) {
fn dump_frame_from_ref(fd: i32, frame: &crate::vm::Py<Frame>) {
let funcname = frame.code.obj_name.as_str();
let filename = frame.code.source_path().as_str();
let lineno = if frame.lasti() == 0 {
Expand All @@ -345,20 +344,23 @@ mod decl {
}

/// Dump traceback for a thread given its frame stack (for cross-thread dumping).
/// # Safety
/// Each `FramePtr` must point to a live frame (caller holds the Mutex).
#[cfg(all(any(unix, windows), feature = "threading"))]
fn dump_traceback_thread_frames(
fd: i32,
thread_id: u64,
is_current: bool,
frames: &[crate::vm::frame::FrameRef],
frames: &[rustpython_vm::vm::FramePtr],
) {
write_thread_id(fd, thread_id, is_current);

if frames.is_empty() {
puts(fd, " <no Python frame>\n");
} else {
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
for fp in frames.iter().rev() {
// SAFETY: caller holds the Mutex, so the owning thread can't pop.
dump_frame_from_ref(fd, unsafe { fp.as_ref() });
}
}
}
Expand All @@ -382,8 +384,9 @@ mod decl {
} else {
puts(fd, "Stack (most recent call first):\n");
let frames = vm.frames.borrow();
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
for fp in frames.iter().rev() {
// SAFETY: the frame is alive while it's in the Vec
dump_frame_from_ref(fd, unsafe { fp.as_ref() });
}
}
}
Expand All @@ -410,7 +413,7 @@ mod decl {
if tid == current_tid {
continue;
}
let frames_guard = slot.lock();
let frames_guard = slot.frames.lock();
dump_traceback_thread_frames(fd, tid, false, &frames_guard);
puts(fd, "\n");
}
Expand All @@ -421,8 +424,8 @@ mod decl {
if frames.is_empty() {
puts(fd, " <no Python frame>\n");
} else {
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
for fp in frames.iter().rev() {
dump_frame_from_ref(fd, unsafe { fp.as_ref() });
}
}
}
Expand All @@ -431,8 +434,8 @@ mod decl {
{
write_thread_id(fd, current_thread_id(), true);
let frames = vm.frames.borrow();
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
for fp in frames.iter().rev() {
dump_frame_from_ref(fd, unsafe { fp.as_ref() });
}
}
}
Expand Down Expand Up @@ -870,7 +873,7 @@ mod decl {
#[cfg(feature = "threading")]
{
for (tid, slot) in &thread_frame_slots {
let frames = slot.lock();
let frames = slot.frames.lock();
dump_traceback_thread_frames(fd, *tid, false, &frames);
}
}
Expand Down
45 changes: 36 additions & 9 deletions crates/vm/src/builtins/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use super::{PyCode, PyDictRef, PyIntRef, PyStrRef};
use crate::{
AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
frame::{Frame, FrameOwner, FrameRef},
function::PySetterValue,
Expand Down Expand Up @@ -195,16 +195,43 @@ impl Py<Frame> {

#[pygetset]
pub fn f_back(&self, vm: &VirtualMachine) -> Option<PyRef<Frame>> {
// TODO: actually store f_back inside Frame struct
let previous = self.previous_frame();
if previous.is_null() {
return None;
}

// get the frame in the frame stack that appears before this one.
// won't work if this frame isn't in the frame stack, hence the todo above
vm.frames
if let Some(frame) = vm
.frames
.borrow()
.iter()
.rev()
.skip_while(|p| !p.is(self.as_object()))
.nth(1)
.cloned()
.find(|fp| {
// SAFETY: the caller keeps the FrameRef alive while it's in the Vec
let py: &crate::Py<Frame> = unsafe { fp.as_ref() };
let ptr: *const Frame = &**py;
core::ptr::eq(ptr, previous)
})
.map(|fp| unsafe { fp.as_ref() }.to_owned())
{
return Some(frame);
}

#[cfg(feature = "threading")]
{
let registry = vm.state.thread_frames.lock();
for slot in registry.values() {
let frames = slot.frames.lock();
// SAFETY: the owning thread can't pop while we hold the Mutex,
// so FramePtr is valid for the duration of the lock.
if let Some(frame) = frames.iter().find_map(|fp| {
let f = unsafe { fp.as_ref() };
let ptr: *const Frame = &**f;
core::ptr::eq(ptr, previous).then(|| f.to_owned())
}) {
return Some(frame);
}
}
}

None
}
}
41 changes: 15 additions & 26 deletions crates/vm/src/coroutine.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
builtins::{PyBaseExceptionRef, PyStrRef},
builtins::PyStrRef,
common::lock::PyMutex,
exceptions::types::PyBaseException,
frame::{ExecutionResult, FrameOwner, FrameRef},
frame::{ExecutionResult, Frame, FrameOwner, FrameRef},
function::OptionalArg,
object::{Traverse, TraverseFn},
object::{PyAtomicRef, Traverse, TraverseFn},
protocol::PyIterReturn,
};
use crossbeam_utils::atomic::AtomicCell;
Expand Down Expand Up @@ -36,15 +36,17 @@ pub struct Coro {
// _weakreflist
name: PyMutex<PyStrRef>,
qualname: PyMutex<PyStrRef>,
exception: PyMutex<Option<PyBaseExceptionRef>>, // exc_state
exception: PyAtomicRef<Option<PyBaseException>>, // exc_state
}

unsafe impl Traverse for Coro {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self.frame.traverse(tracer_fn);
self.name.traverse(tracer_fn);
self.qualname.traverse(tracer_fn);
self.exception.traverse(tracer_fn);
if let Some(exc) = self.exception.deref() {
exc.traverse(tracer_fn);
}
}
Comment on lines +39 to 50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "*.rs" -type f | xargs grep -l "struct PyAtomicRef" | head -5

Repository: RustPython/RustPython

Length of output: 94


🏁 Script executed:

rg -n "struct PyAtomicRef|type PyAtomicRef" --type rs

Repository: RustPython/RustPython

Length of output: 91


🏁 Script executed:

# Get context around lines 39-50 and 64-74 in coroutine.rs
cat -n crates/vm/src/coroutine.rs | head -80

Repository: RustPython/RustPython

Length of output: 3152


🏁 Script executed:

# Search for PyAtomicRef and Traverse definitions
rg -n "trait Traverse|fn traverse" crates/vm/src --type rs -A 3

Repository: RustPython/RustPython

Length of output: 91


🏁 Script executed:

rg "struct PyAtomicRef" crates/vm/src/object/ -A 10

Repository: RustPython/RustPython

Length of output: 725


🏁 Script executed:

rg "impl.*PyAtomicRef" crates/vm/src/object/ -A 5

Repository: RustPython/RustPython

Length of output: 5675


🏁 Script executed:

rg "fn deref" crates/vm/src/object/ -B 2 -A 5

Repository: RustPython/RustPython

Length of output: 4162


🏁 Script executed:

rg "trait Traverse" crates/vm/src/object/ -A 5

Repository: RustPython/RustPython

Length of output: 678


🏁 Script executed:

rg -n "run_with_context\|exception.*swap\|exception.*deref" crates/vm/src/coroutine.rs -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

cat -n crates/vm/src/coroutine.rs | sed -n '85,130p'

Repository: RustPython/RustPython

Length of output: 1825


🏁 Script executed:

rg -n "\.exception\." crates/vm/src/coroutine.rs

Repository: RustPython/RustPython

Length of output: 183


🏁 Script executed:

# Check how Traverse is called (where traversal happens)
rg -n "\.traverse\(" crates/vm/src/ -B 2 -A 2 | head -50

Repository: RustPython/RustPython

Length of output: 3083


🏁 Script executed:

rg -n "fn.*gc|GC" crates/vm/src/ --type rs | head -20

Repository: RustPython/RustPython

Length of output: 91


🏁 Script executed:

rg -n "\.traverse(" crates/vm/src/object/ -B 3 -A 1 | head -80

Repository: RustPython/RustPython

Length of output: 137


🏁 Script executed:

# Look for GC collection cycle and when it calls traverse
rg -n "impl.*Traverse\|traverse\(" crates/vm/src/gc.rs 2>/dev/null | head -30

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

find crates -name "gc.rs" -type f

Repository: RustPython/RustPython

Length of output: 91


🏁 Script executed:

rg -n "traverse" crates/vm/src/stdlib/gc.rs -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Look at coroutine integration with GC
cat crates/vm/src/stdlib/gc.rs | head -100

Repository: RustPython/RustPython

Length of output: 3099


🏁 Script executed:

# Search for when GC collect is called during execution
rg -n "collect\|gc_collect" crates/vm/src/ --type-list

Repository: RustPython/RustPython

Length of output: 454


🏁 Script executed:

# Better search: look for Traverse implementations and understand execution model
rg -n "unsafe impl Traverse" crates/vm/src/ | head -30

Repository: RustPython/RustPython

Length of output: 2313


🏁 Script executed:

# Let's look at the full context of run_with_context and exception handling
cat -n crates/vm/src/coroutine.rs | sed -n '90,120p'

Repository: RustPython/RustPython

Length of output: 1298


🏁 Script executed:

# Check how resume_gen_frame works
rg -n "fn resume_gen_frame" crates/vm/src/ -A 15

Repository: RustPython/RustPython

Length of output: 1122


🏁 Script executed:

# Look at how exception is used in the frame/vm context
rg -n "current_exception\|set_exception" crates/vm/src/ -B 1 -A 2 | head -80

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Check if there are any comments or discussions about atomic ref safety
rg -n "PyAtomicRef" crates/vm/src/ -B 2 -A 2 | head -60

Repository: RustPython/RustPython

Length of output: 3902


🏁 Script executed:

# Check if there's any GC triggering during frame execution
rg -n "gc_collect\|collect_force" crates/vm/src/ -B 3 -A 3 | head -80

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Look at how the running flag's exclusive access guarantee applies to Traverse
cat -n crates/vm/src/coroutine.rs | sed -n '42,52p'

Repository: RustPython/RustPython

Length of output: 460


🏁 Script executed:

# Check the PyAtomicRef safety documentation and contracts
cat -n crates/vm/src/object/ext.rs | sed -n '244,320p'

Repository: RustPython/RustPython

Length of output: 3032


🏁 Script executed:

# Get more context on PyAtomicRef swap and deref for Option<T>
cat -n crates/vm/src/object/ext.rs | sed -n '340,380p'

Repository: RustPython/RustPython

Length of output: 1714


🏁 Script executed:

# Check the full Traverse trait documentation
cat -n crates/vm/src/object/traverse.rs | head -80

Repository: RustPython/RustPython

Length of output: 3580


🏁 Script executed:

# Verify: Can GC run while running flag is set?
rg -n "running.store\|running.compare_exchange\|running.load" crates/vm/src/coroutine.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Look at how GC traversal interacts with running coroutines
cat -n crates/vm/src/gc_state.rs 2>/dev/null | head -50

Repository: RustPython/RustPython

Length of output: 2071


🏁 Script executed:

# Search for where PyAtomicRef deref safety is discussed or tested
rg -n "deref\|swap" crates/vm/src/object/ext.rs | grep -A2 -B2 "unsafe"

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Verify: is there any GC trigger during frame execution that might call traverse?
rg -n "gc_collect\|trigger.*gc\|AUTO_GC" crates/vm/src/ | head -20

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Check if there are existing tests or comments about concurrent traverse/swap safety
rg -n "test.*traverse\|test.*atomic" crates/vm/src/ -B 2 -A 5 | head -40

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Check if exception.deref could be called while exception is being swapped
# Look at the exact sequence in run_with_context more carefully
cat -n crates/vm/src/coroutine.rs | sed -n '99,116p'

Repository: RustPython/RustPython

Length of output: 926


🏁 Script executed:

# Verify the AcqRel vs Relaxed ordering implications
rg -n "Ordering::" crates/vm/src/object/ext.rs | grep -A2 -B2 "swap\|deref"

Repository: RustPython/RustPython

Length of output: 846


🏁 Script executed:

# Check if there's a panic-safety issue with the running flag cleanup
cat -n crates/vm/src/coroutine.rs | sed -n '90,116p'

Repository: RustPython/RustPython

Length of output: 1213


🏁 Script executed:

# Check for panic-safety guard patterns in similar code
rg -n "scopeguard\|guard\|Drop" crates/vm/src/coroutine.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Look for how other code handles the running flag or similar state cleanup
rg -n "compare_exchange.*running\|running.*store" crates/vm/src/ -B 3 -A 3 | head -50

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Check if resume_gen_frame could panic and what the consequences would be
cat -n crates/vm/src/vm/mod.rs | sed -n '1091,1130p'

Repository: RustPython/RustPython

Length of output: 1973


Wrap running flag reset in a guard to ensure panic-safety.

If resume_gen_frame or the inner closure panics, line 114 is never executed, leaving running=true permanently. This causes subsequent send() calls to immediately fail with "already executing". Use scopeguard::defer! or similar to guarantee running.store(false) is called on all exit paths, matching the pattern already used in resume_gen_frame() itself (lines 1121-1129).

🤖 Prompt for AI Agents
In `@crates/vm/src/coroutine.rs` around lines 39 - 50, The code sets the
coroutine's running flag to true but resets it with a plain store(false) that
won't run if resume_gen_frame or its inner closure panics; update the code
around the running flag in the function that calls resume_gen_frame (the
send()/resume path) to install a panic-safe guard (e.g. scopeguard::defer! or a
RAII guard) immediately after setting running.store(true) so that
running.store(false) is executed on all exit paths; reference the running
atomic, the send()/resume function, and resume_gen_frame to locate where to add
the guard and ensure the current plain running.store(false) is removed or made
unreachable by the new guard.

}

Expand All @@ -65,7 +67,7 @@ impl Coro {
frame,
closed: AtomicCell::new(false),
running: AtomicCell::new(false),
exception: PyMutex::default(),
exception: PyAtomicRef::from(None),
name: PyMutex::new(name),
qualname: PyMutex::new(qualname),
}
Expand All @@ -92,33 +94,20 @@ impl Coro {
func: F,
) -> PyResult<ExecutionResult>
where
F: FnOnce(FrameRef) -> PyResult<ExecutionResult>,
F: FnOnce(&Py<Frame>) -> PyResult<ExecutionResult>,
{
if self.running.compare_exchange(false, true).is_err() {
return Err(vm.new_value_error(format!("{} already executing", gen_name(jen, vm))));
}

// swap exception state
// Get generator's saved exception state from last yield
let gen_exc = self.exception.lock().take();

// Use a slot to capture generator's exception state before with_frame pops
let exception_slot = &self.exception;
// SAFETY: running.compare_exchange guarantees exclusive access
let gen_exc = unsafe { self.exception.swap(None) };
let exception_ptr = &self.exception as *const PyAtomicRef<Option<PyBaseException>>;

// Run the generator frame
// with_frame does push_exception(None) which creates a new exception context
// The caller's exception remains in the chain via prev, so topmost_exception()
// will find it if generator's exception is None
let result = vm.with_frame(self.frame.clone(), |f| {
// with_frame pushed None, creating: { exc: None, prev: caller's exc_info }
// Pop None and push generator's exception instead
// This maintains the chain: { exc: gen_exc, prev: caller's exc_info }
vm.pop_exception();
vm.push_exception(gen_exc);
let result = vm.resume_gen_frame(&self.frame, gen_exc, |f| {
let result = func(f);
// Save generator's exception state BEFORE with_frame pops
// This is the generator's current exception context
*exception_slot.lock() = vm.current_exception();
// SAFETY: exclusive access guaranteed by running flag
let _old = unsafe { (*exception_ptr).swap(vm.current_exception()) };
result
});

Expand Down
43 changes: 38 additions & 5 deletions crates/vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ impl ExecutingFrame<'_> {
// Execute until return or exception:
let instructions = &self.code.instructions;
let mut arg_state = bytecode::OpArgState::default();
let mut prev_line: usize = 0;
let mut prev_line: u32 = 0;
loop {
let idx = self.lasti() as usize;
// Fire 'line' trace event when line number changes.
Expand All @@ -472,9 +472,9 @@ impl ExecutingFrame<'_> {
if vm.use_tracing.get()
&& !vm.is_none(&self.object.trace.lock())
&& let Some((loc, _)) = self.code.locations.get(idx)
&& loc.line.get() != prev_line
&& loc.line.get() as u32 != prev_line
{
prev_line = loc.line.get();
prev_line = loc.line.get() as u32;
vm.trace_event(crate::protocol::TraceEvent::Line, None)?;
}
self.update_lasti(|i| *i += 1);
Expand Down Expand Up @@ -543,13 +543,16 @@ impl ExecutingFrame<'_> {
// Check if this is a RERAISE instruction
// Both AnyInstruction::Raise { kind: Reraise/ReraiseFromStack } and
// AnyInstruction::Reraise are reraise operations that should not add
// new traceback entries
// new traceback entries.
// EndAsyncFor and CleanupThrow also re-raise non-matching exceptions.
let is_reraise = match op {
Instruction::RaiseVarargs { kind } => matches!(
kind.get(arg),
bytecode::RaiseKind::BareRaise | bytecode::RaiseKind::ReraiseFromStack
),
Instruction::Reraise { .. } => true,
Instruction::Reraise { .. }
| Instruction::EndAsyncFor
| Instruction::CleanupThrow => true,
_ => false,
};
Comment on lines 543 to 557
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the code changes and find where these instructions are used
rg -n "EndAsyncFor|CleanupThrow" crates/vm/src/ -A 3 -B 3

Repository: RustPython/RustPython

Length of output: 2834


🏁 Script executed:

# Find test coverage for these instructions
rg -n "EndAsyncFor|CleanupThrow|end_async_for|cleanup_throw" --type py tests/ crates/

Repository: RustPython/RustPython

Length of output: 111


🏁 Script executed:

# Look at the full context of frame.rs around the changed lines
sed -n '530,570p' crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 2330


🏁 Script executed:

# Check if there are any comments or documentation about reraise behavior
rg -n "reraise|RERAISE" crates/vm/src/frame.rs -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 3124


🏁 Script executed:

# Search for where RaiseVarargs and Reraise instructions are handled
rg -n "Instruction::Reraise|Instruction::RaiseVarargs" crates/vm/src/frame.rs -B 2 -A 5

Repository: RustPython/RustPython

Length of output: 2386


🏁 Script executed:

# Find tests related to async for loops and exception handling
find . -name "*.py" -type f | xargs grep -l "async for\|EndAsyncFor" 2>/dev/null | head -20

Repository: RustPython/RustPython

Length of output: 703


🏁 Script executed:

# Search for existing exception handling tests
find . -name "*.py" -type f | xargs grep -l "StopAsyncIteration\|cleanup" 2>/dev/null | head -20

Repository: RustPython/RustPython

Length of output: 636


🏁 Script executed:

# Look for generator/async tests
fd -e py | xargs grep -l "async def\|async for" | head -10

Repository: RustPython/RustPython

Length of output: 337


🏁 Script executed:

# Check bytecode instruction definitions
rg -n "EndAsyncFor|CleanupThrow|CLEANUP_THROW|END_ASYNC_FOR" crates/ --type rust -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 9658


🏁 Script executed:

# Look at EndAsyncFor implementation details
sed -n '1139,1165p' crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 1186


🏁 Script executed:

# Look at CleanupThrow implementation details
sed -n '2257,2280p' crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 1428


Verify targeted test coverage for reraise paths in EndAsyncFor and CleanupThrow.

The classification of these instructions as reraise-like is correct: both conditionally re-raise when their expected exception (StopAsyncIteration/StopIteration) is not matched. When they do re-raise, they should not add new traceback entries, matching CPython semantics.

However, ensure targeted coverage exists for the re-raise paths:

  • EndAsyncFor with non-StopAsyncIteration exceptions
  • CleanupThrow with non-StopIteration exceptions

Perform a clean build before testing: rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r

🤖 Prompt for AI Agents
In `@crates/vm/src/frame.rs` around lines 543 - 557, The is_reraise classification
correctly treats Instruction::EndAsyncFor and Instruction::CleanupThrow as
re-raise-like, but you must add targeted tests that exercise the re-raise branch
for each: one test that runs EndAsyncFor with a non-StopAsyncIteration exception
and asserts no new traceback entry is added, and one test that runs CleanupThrow
with a non-StopIteration exception and asserts the same; add these
unit/integration tests around the VM instruction execution harness (target the
is_reraise behavior) and run a clean build before testing (rm -r
target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r) to
ensure fresh artifacts.


Expand Down Expand Up @@ -653,6 +656,19 @@ impl ExecutingFrame<'_> {
Ok(())
};
if let Err(err) = close_result {
let idx = self.lasti().saturating_sub(1) as usize;
if idx < self.code.locations.len() {
let (loc, _end_loc) = self.code.locations[idx];
let next = err.__traceback__();
let new_traceback = PyTraceback::new(
next,
self.object.to_owned(),
idx as u32 * 2,
loc.line,
);
err.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}

self.push_value(vm.ctx.none());
vm.contextualize_exception(&err);
return match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
Expand All @@ -678,6 +694,23 @@ impl ExecutingFrame<'_> {
Either::B(meth) => meth.call((exc_type, exc_val, exc_tb), vm),
};
return ret.map(ExecutionResult::Yield).or_else(|err| {
// Add traceback entry for the yield-from/await point.
// gen_send_ex2 resumes the frame with a pending exception,
// which goes through error: → PyTraceBack_Here. We add the
// entry here before calling unwind_blocks.
let idx = self.lasti().saturating_sub(1) as usize;
if idx < self.code.locations.len() {
let (loc, _end_loc) = self.code.locations[idx];
let next = err.__traceback__();
let new_traceback = PyTraceback::new(
next,
self.object.to_owned(),
idx as u32 * 2,
loc.line,
);
err.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}

self.push_value(vm.ctx.none());
vm.contextualize_exception(&err);
match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
Expand Down
9 changes: 4 additions & 5 deletions crates/vm/src/protocol/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
builtins::{PyBoundMethod, PyFunction},
function::{FuncArgs, IntoFuncArgs},
types::GenericMethod,
{AsObject, PyObject, PyObjectRef, PyResult, VirtualMachine},
{PyObject, PyObjectRef, PyResult, VirtualMachine},
};

impl PyObject {
Expand Down Expand Up @@ -111,12 +111,11 @@ impl VirtualMachine {
return Ok(());
}

let frame_ref = self.current_frame();
if frame_ref.is_none() {
let Some(frame_ref) = self.current_frame() else {
return Ok(());
}
};

let frame = frame_ref.unwrap().as_object().to_owned();
let frame: PyObjectRef = frame_ref.into();
let event = self.ctx.new_str(event.to_string()).into();
let args = vec![frame, event, arg.unwrap_or_else(|| self.ctx.none())];

Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/stdlib/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ mod builtins {
)
}
None => (
vm.current_globals().clone(),
vm.current_globals(),
if let Some(locals) = self.locals {
locals
} else {
Expand Down Expand Up @@ -503,7 +503,7 @@ mod builtins {

#[pyfunction]
fn globals(vm: &VirtualMachine) -> PyDictRef {
vm.current_globals().clone()
vm.current_globals()
}

#[pyfunction]
Expand Down
Loading
Loading
X Tutup