Add new_payload_exception helper for constructing built-in payload exceptions - #8403
Add new_payload_exception helper for constructing built-in payload exceptions#8403kangdora wants to merge 7 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughThe VM now provides typed payload exception construction. OSError, BlockingIOError, SystemExit, and StopIteration paths use this helper instead of generic exception invocation. ChangesPayload exception construction
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ExceptionPath
participant VirtualMachine
participant PyException
ExceptionPath->>VirtualMachine: new_payload_exception(exception type, arguments)
VirtualMachine->>PyException: py_new(arguments)
VirtualMachine->>PyException: slot_init(arguments)
PyException-->>VirtualMachine: typed exception
VirtualMachine-->>ExceptionPath: upcast exception
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 |
|
@kangdora do you have any concern to work more on this patch? |
|
Yeah, one concern. Since Honestly this is a little beyond my current level, so instead of changing everything at once I've been going through the call sites to figure out which ones are actually safe to touch first. Where I've landed so far:
So rather than limiting it upfront, I'd like to keep working through these gradually as I understand the area better. If I've misjudged any of these boundaries, I'd really appreciate a correction. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
crates/vm/src/vm/vm_new.rs (1)
357-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a debug-mode type/class consistency guard.
new_payload_exceptionbuilds aPyRef<T>from an arbitrarycls: PyTypeRefargument. The function relies only on the doc comment to state thatTmust match the exact built-in type.new_exceptionin this same file guards this exact class of misuse with adebug_assert_eq!onbasicsize. Add a similar guard here.Do this before merging the payload into
into_ref_with_type_lazy_dict. Comparecls.slots.basicsizeagainstcore::mem::size_of::<T>()in adebug_assert_eq!, with a message that points callers to the doc comment.♻️ Proposed defensive guard
pub fn new_payload_exception<T>(&self, cls: PyTypeRef, args: FuncArgs) -> PyResult<PyRef<T>> where T: Constructor<Args = FuncArgs> + Initializer, { + debug_assert_eq!( + cls.slots.basicsize, + core::mem::size_of::<T>(), + "vm.new_payload_exception::<{}>() called with mismatched type '{}'", + core::any::type_name::<T>(), + cls.name() + ); let payload = T::py_new(&cls, args.clone(), self)?; let exc = payload.into_ref_with_type_lazy_dict(self, cls)?; T::slot_init(exc.as_object().to_owned(), args, self)?; Ok(exc) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/vm/vm_new.rs` around lines 357 - 369, Add a debug_assert_eq! in new_payload_exception immediately before into_ref_with_type_lazy_dict, comparing cls.slots.basicsize with core::mem::size_of::<T>() and including a message directing callers to the doc comment.crates/vm/src/stdlib/sys.rs (1)
779-783: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
SystemExitconstruction across four files. Each site buildsSystemExitthrough the identical sequencenew_payload_exception::<PySystemExit>(vm.ctx.exceptions.system_exit.to_owned(), <args>)?.upcast(), differing only in theargsvalue. Extract one shared helper instead of repeating the construction logic.
crates/vm/src/stdlib/sys.rs#L779-L783: replace the inline construction with a call to a newvm.new_system_exit(args.into())helper.crates/vm/src/stdlib/_thread.rs#L638-L643: replace the inline construction withvm.new_system_exit(vec![].into()).crates/vm/src/stdlib/builtins.rs#L1044-L1049: replace the inline construction withvm.new_system_exit(vec![code].into()).crates/vm/src/vm/mod.rs#L2172-L2177: replace the inline construction withself.new_system_exit(vec![].into()).Add the helper to
crates/vm/src/vm/vm_new.rs, next tonew_stop_iteration:pub fn new_system_exit(&self, args: FuncArgs) -> PyBaseExceptionRef { self.new_payload_exception::<PySystemExit>( self.ctx.exceptions.system_exit.to_owned(), args, ) .expect("SystemExit is a BaseException Subclass.") .upcast() }As per coding guidelines, "When branches differ only in a value but share logic, extract the differing value and invoke the common logic once."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/stdlib/sys.rs` around lines 779 - 783, Extract the repeated SystemExit construction into new_system_exit in crates/vm/src/vm/vm_new.rs beside new_stop_iteration, accepting FuncArgs and returning PyBaseExceptionRef with the existing payload-exception behavior. Replace the inline construction at crates/vm/src/stdlib/sys.rs:779-783 with vm.new_system_exit(args.into()), at crates/vm/src/stdlib/_thread.rs:638-643 with vm.new_system_exit(vec![].into()), at crates/vm/src/stdlib/builtins.rs:1044-1049 with vm.new_system_exit(vec![code].into()), and at crates/vm/src/vm/mod.rs:2172-2177 with self.new_system_exit(vec![].into()).Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/vm/src/stdlib/sys.rs`:
- Around line 779-783: Extract the repeated SystemExit construction into
new_system_exit in crates/vm/src/vm/vm_new.rs beside new_stop_iteration,
accepting FuncArgs and returning PyBaseExceptionRef with the existing
payload-exception behavior. Replace the inline construction at
crates/vm/src/stdlib/sys.rs:779-783 with vm.new_system_exit(args.into()), at
crates/vm/src/stdlib/_thread.rs:638-643 with vm.new_system_exit(vec![].into()),
at crates/vm/src/stdlib/builtins.rs:1044-1049 with
vm.new_system_exit(vec![code].into()), and at crates/vm/src/vm/mod.rs:2172-2177
with self.new_system_exit(vec![].into()).
In `@crates/vm/src/vm/vm_new.rs`:
- Around line 357-369: Add a debug_assert_eq! in new_payload_exception
immediately before into_ref_with_type_lazy_dict, comparing cls.slots.basicsize
with core::mem::size_of::<T>() and including a message directing callers to the
doc comment.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5f03cc7c-268b-4a89-aa69-a4a2bf0b2e9d
📒 Files selected for processing (7)
crates/vm/src/exceptions.rscrates/vm/src/stdlib/_io.rscrates/vm/src/stdlib/_thread.rscrates/vm/src/stdlib/builtins.rscrates/vm/src/stdlib/sys.rscrates/vm/src/vm/mod.rscrates/vm/src/vm/vm_new.rs
youknowone
left a comment
There was a problem hiding this comment.
Thank you! The changes look good in general, but I have a few suggestions. Please check them
| self.ctx.exceptions.stop_iteration.to_owned(), | ||
| args, | ||
| ) | ||
| .expect("StopIteration is a BaseException Subclass.") |
There was a problem hiding this comment.
new_payload_exception now can raise exceptions by multiple reasons, so this except message doesn't work. I am even not sure expect is good idea anymore.
| .new_payload_exception::<PySystemExit>( | ||
| vm.ctx.exceptions.system_exit.to_owned(), | ||
| vec![].into(), | ||
| ) | ||
| .expect("SystemExit is a BaseException Subclass.") |
There was a problem hiding this comment.
if this pattern repeats this many times, I prefer to add vm.new_system_exit() rather than manual building for multiple times.
| cls.name() | ||
| ); | ||
| let payload = T::py_new(&cls, args.clone(), self)?; | ||
| let exc = payload.into_ref_with_type_lazy_dict(self, cls)?; |
There was a problem hiding this comment.
calling into_ref_with_type_lazy_dict came from OSErrorBuilder. I don't know this part well, so I'd like to ask if this is safe for every exception types or not.
Follow-up to #8348.
Summary
Adds
new_payload_exception, a generic helper for constructing built-in payload exceptions (a#[repr(C)]struct with extra fields, e.g.SystemExit.code,StopIteration.value, theOSErrorfields) whose type is known at compile time. It builds them directly withpy_new+slot_initinstead of going throughPyType::call, and replaces the ad hoc construction in the internal builders.Background
Payload exceptions were introduced in #8282 (
SystemExit.code) and #8301 (StopIteration.value), where the value moved from the instance dict into a real struct field. Constructing them currently happens either throughPyType::call(viainvoke_exception) or through a bespoke per-type builder likeOSErrorBuilder. This adds a single direct path for the internal builders that know their exact type at compile time:new_exceptionstays as the thin, restricted fast path for payload-free types.invoke_exceptionstays for construction where the concrete type is only known at runtime and a compile-timeTcannot express it:raise <expr>, the C-API entry, and ctypesCOMError.BaseExceptionGroupis left as is too, since it isrepr(transparent)and carries no extra payload.Call sites
This routes the internal builders that know their type at compile time through the helper:
new_stop_iterationandOSErrorBuilder, the simplest and most complex payload exceptions respectivelySystemExitraise sites: builtinsexit,sys.exit,_thread.exit, and the finalization check incheck_signalsBlockingIOErrorraise sites in_ioPyOSError::slot_new. The target subtype is chosen at runtime, but every result oferrno_to_exc_typeis arepr(transparent)OSError subtype, soPyOSErroris a valid payload type for all of themWhat this buys
The main effect is on
invoke_exception. Once these sites move off it,invoke_exceptionis left holding only construction that is genuinely runtime-typed (raise <expr>, C-API,COMError), so its role is explicit rather than a catch-all. Going through the helper also skipsPyType::call, which for each construction avoids two slot lookups, the__new__and__init__indirect calls, an args clone, and a subclass check. For the hot sites (StopIterationon iterator exhaustion, and the errno OSError path) that saving is real per call. For the cold raise sites (SystemExit,BlockingIOError) it is not measurable.Notes
Two things I would welcome feedback on:
invoke_exceptionto the runtime-typed path, but those raise sites are rare. Happy to land only the higher-value sites if the consistency-only ones aren't worth the churn.PyBaseExceptionRefupcast the helper'sPyRef<T>, and.upcast()goes through a runtime downcast (the zero-costupcast_ref/to_baseonly yield&Py<_>). So the migration tradesPyType::call's slot dispatch for a smaller runtime downcast: a net reduction in steps where it is hot, and a wash where it is cold. If there is a zero-cost owned upcast I missed, I would rather use that.Assisted-by: claude-fable-5
Summary by CodeRabbit
OSError,BlockingIOError,SystemExit, andStopIteration.