-
Notifications
You must be signed in to change notification settings - Fork 1.4k
traverse and clear #6780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
traverse and clear #6780
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -574,51 +574,80 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok | |||||||||
| )?; | ||||||||||
|
|
||||||||||
| const ALLOWED_TRAVERSE_OPTS: &[&str] = &["manual"]; | ||||||||||
| // try to know if it have a `#[pyclass(trace)]` exist on this struct | ||||||||||
| // TODO(discord9): rethink on auto detect `#[Derive(PyTrace)]` | ||||||||||
|
|
||||||||||
| // 1. no `traverse` at all: generate a dummy try_traverse | ||||||||||
| // 2. `traverse = "manual"`: generate a try_traverse, but not #[derive(Traverse)] | ||||||||||
| // 3. `traverse`: generate a try_traverse, and #[derive(Traverse)] | ||||||||||
| let (maybe_trace_code, derive_trace) = { | ||||||||||
| if class_meta.inner()._has_key("traverse")? { | ||||||||||
| let maybe_trace_code = quote! { | ||||||||||
| impl ::rustpython_vm::object::MaybeTraverse for #ident { | ||||||||||
| const IS_TRACE: bool = true; | ||||||||||
| fn try_traverse(&self, tracer_fn: &mut ::rustpython_vm::object::TraverseFn) { | ||||||||||
| ::rustpython_vm::object::Traverse::traverse(self, tracer_fn); | ||||||||||
| } | ||||||||||
| // Generate MaybeTraverse impl with both traverse and clear support | ||||||||||
| // | ||||||||||
| // For traverse: | ||||||||||
| // 1. no `traverse` at all: HAS_TRAVERSE = false, try_traverse does nothing | ||||||||||
| // 2. `traverse = "manual"`: HAS_TRAVERSE = true, but no #[derive(Traverse)] | ||||||||||
| // 3. `traverse`: HAS_TRAVERSE = true, and #[derive(Traverse)] | ||||||||||
| // | ||||||||||
| // For clear (tp_clear): | ||||||||||
| // 1. no `clear`: HAS_CLEAR = HAS_TRAVERSE (default: same as traverse) | ||||||||||
| // 2. `clear` or `clear = true`: HAS_CLEAR = true, try_clear calls Traverse::clear | ||||||||||
| // 3. `clear = false`: HAS_CLEAR = false (rare: traverse without clear) | ||||||||||
| let has_traverse = class_meta.inner()._has_key("traverse")?; | ||||||||||
| let has_clear = if class_meta.inner()._has_key("clear")? { | ||||||||||
| // If clear attribute is present, use its value | ||||||||||
| class_meta.inner()._bool("clear")? | ||||||||||
| } else { | ||||||||||
| // If clear attribute is absent, default to same as traverse | ||||||||||
| has_traverse | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| let derive_trace = if has_traverse { | ||||||||||
| // _optional_str returns Err when key exists without value (e.g., `traverse` vs `traverse = "manual"`) | ||||||||||
| // We want to derive Traverse in that case, so we handle Err as Ok(None) | ||||||||||
| let value = class_meta.inner()._optional_str("traverse").ok().flatten(); | ||||||||||
| if let Some(s) = value { | ||||||||||
| if !ALLOWED_TRAVERSE_OPTS.contains(&s.as_str()) { | ||||||||||
| bail_span!( | ||||||||||
| item, | ||||||||||
| "traverse attribute only accept {ALLOWED_TRAVERSE_OPTS:?} as value or no value at all", | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| assert_eq!(s, "manual"); | ||||||||||
| quote! {} | ||||||||||
| } else { | ||||||||||
| quote! {#[derive(Traverse)]} | ||||||||||
| } | ||||||||||
| } else { | ||||||||||
| quote! {} | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| let maybe_traverse_code = { | ||||||||||
| let try_traverse_body = if has_traverse { | ||||||||||
| quote! { | ||||||||||
| ::rustpython_vm::object::Traverse::traverse(self, tracer_fn); | ||||||||||
| } | ||||||||||
| } else { | ||||||||||
| quote! { | ||||||||||
| // do nothing | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| let try_clear_body = if has_clear { | ||||||||||
| quote! { | ||||||||||
| ::rustpython_vm::object::Traverse::clear(self, out); | ||||||||||
| } | ||||||||||
| } else { | ||||||||||
| quote! { | ||||||||||
| // do nothing | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| quote! { | ||||||||||
| impl ::rustpython_vm::object::MaybeTraverse for #ident { | ||||||||||
| const HAS_TRAVERSE: bool = #has_traverse; | ||||||||||
| const HAS_CLEAR: bool = #has_clear; | ||||||||||
|
|
||||||||||
| fn try_traverse(&self, tracer_fn: &mut ::rustpython_vm::object::TraverseFn) { | ||||||||||
| #try_traverse_body | ||||||||||
|
Comment on lines
+643
to
+644
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Search for TraverseFn definition
rg -n "type TraverseFn|struct TraverseFn|trait TraverseFn" crates/vm/src/object -A 3 -B 1Repository: RustPython/RustPython Length of output: 186 🏁 Script executed: # Check current code at lines 643-644 in the file
sed -n '640,650p' crates/derive-impl/src/pyclass.rsRepository: RustPython/RustPython Length of output: 186 🏁 Script executed: # Search for other uses of TraverseFn in the codebase to see patterns
rg -n "TraverseFn" crates/ --type rs | head -20Repository: RustPython/RustPython Length of output: 186 🌐 Web query:
💡 Result: Definition (from rustpython-vm crate): Sources:
🌐 Web query:
💡 Result: I can look that up in the RustPython source (to give the exact current Rust signature). Do you want me to fetch the signature from the repository (GitHub/docs.rs) now? Fix the missing lifetime on 🔧 Proposed fix- fn try_traverse(&self, tracer_fn: &mut ::rustpython_vm::object::TraverseFn) {
+ fn try_traverse(&self, tracer_fn: &mut ::rustpython_vm::object::TraverseFn<'_>) {
`#try_traverse_body`
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
| }; | ||||||||||
| // if the key `traverse` exist but not as key-value, _optional_str return Err(...) | ||||||||||
| // so we need to check if it is Ok(Some(...)) | ||||||||||
| let value = class_meta.inner()._optional_str("traverse"); | ||||||||||
| let derive_trace = if let Ok(Some(s)) = value { | ||||||||||
| if !ALLOWED_TRAVERSE_OPTS.contains(&s.as_str()) { | ||||||||||
| bail_span!( | ||||||||||
| item, | ||||||||||
| "traverse attribute only accept {ALLOWED_TRAVERSE_OPTS:?} as value or no value at all", | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| fn try_clear(&mut self, out: &mut ::std::vec::Vec<::rustpython_vm::PyObjectRef>) { | ||||||||||
| #try_clear_body | ||||||||||
| } | ||||||||||
| assert_eq!(s, "manual"); | ||||||||||
| quote! {} | ||||||||||
| } else { | ||||||||||
| quote! {#[derive(Traverse)]} | ||||||||||
| }; | ||||||||||
| (maybe_trace_code, derive_trace) | ||||||||||
| } else { | ||||||||||
| ( | ||||||||||
| // a dummy impl, which do nothing | ||||||||||
| // #attrs | ||||||||||
| quote! { | ||||||||||
| impl ::rustpython_vm::object::MaybeTraverse for #ident { | ||||||||||
| fn try_traverse(&self, tracer_fn: &mut ::rustpython_vm::object::TraverseFn) { | ||||||||||
| // do nothing | ||||||||||
| } | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| quote! {}, | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
|
|
@@ -675,7 +704,7 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok | |||||||||
| let ret = quote! { | ||||||||||
| #derive_trace | ||||||||||
| #item | ||||||||||
| #maybe_trace_code | ||||||||||
| #maybe_traverse_code | ||||||||||
| #class_def | ||||||||||
| #impl_payload | ||||||||||
| #empty_impl | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -51,6 +51,70 @@ unsafe impl Traverse for PyFunction { | |||||||||||||||||||||
| closure.as_untyped().traverse(tracer_fn); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| self.defaults_and_kwdefaults.traverse(tracer_fn); | ||||||||||||||||||||||
| // Traverse additional fields that may contain references | ||||||||||||||||||||||
| self.type_params.lock().traverse(tracer_fn); | ||||||||||||||||||||||
| self.annotations.lock().traverse(tracer_fn); | ||||||||||||||||||||||
| self.module.lock().traverse(tracer_fn); | ||||||||||||||||||||||
| self.doc.lock().traverse(tracer_fn); | ||||||||||||||||||||||
|
Comment on lines
+54
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid blocking locks in
✅ Safer traversal (non-blocking)- self.type_params.lock().traverse(tracer_fn);
- self.annotations.lock().traverse(tracer_fn);
- self.module.lock().traverse(tracer_fn);
- self.doc.lock().traverse(tracer_fn);
+ self.type_params.traverse(tracer_fn);
+ self.annotations.traverse(tracer_fn);
+ self.module.traverse(tracer_fn);
+ self.doc.traverse(tracer_fn);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| fn clear(&mut self, out: &mut Vec<crate::PyObjectRef>) { | ||||||||||||||||||||||
| // Pop closure if present (equivalent to Py_CLEAR(func_closure)) | ||||||||||||||||||||||
| if let Some(closure) = self.closure.take() { | ||||||||||||||||||||||
| out.push(closure.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Pop defaults and kwdefaults | ||||||||||||||||||||||
| if let Some(mut guard) = self.defaults_and_kwdefaults.try_lock() { | ||||||||||||||||||||||
| if let Some(defaults) = guard.0.take() { | ||||||||||||||||||||||
| out.push(defaults.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if let Some(kwdefaults) = guard.1.take() { | ||||||||||||||||||||||
| out.push(kwdefaults.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Clear annotations and annotate (Py_CLEAR) | ||||||||||||||||||||||
| if let Some(mut guard) = self.annotations.try_lock() | ||||||||||||||||||||||
| && let Some(annotations) = guard.take() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| out.push(annotations.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if let Some(mut guard) = self.annotate.try_lock() | ||||||||||||||||||||||
| && let Some(annotate) = guard.take() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| out.push(annotate); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Clear module, doc, and type_params (Py_CLEAR) | ||||||||||||||||||||||
| if let Some(mut guard) = self.module.try_lock() { | ||||||||||||||||||||||
| let old_module = | ||||||||||||||||||||||
| std::mem::replace(&mut *guard, Context::genesis().none.to_owned().into()); | ||||||||||||||||||||||
| out.push(old_module); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if let Some(mut guard) = self.doc.try_lock() { | ||||||||||||||||||||||
| let old_doc = std::mem::replace(&mut *guard, Context::genesis().none.to_owned().into()); | ||||||||||||||||||||||
| out.push(old_doc); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if let Some(mut guard) = self.type_params.try_lock() { | ||||||||||||||||||||||
| let old_type_params = | ||||||||||||||||||||||
| std::mem::replace(&mut *guard, Context::genesis().empty_tuple.to_owned()); | ||||||||||||||||||||||
| out.push(old_type_params.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Replace name and qualname with empty string to break potential str subclass cycles | ||||||||||||||||||||||
| // name and qualname could be str subclasses, so they could have reference cycles | ||||||||||||||||||||||
| if let Some(mut guard) = self.name.try_lock() { | ||||||||||||||||||||||
| let old_name = std::mem::replace(&mut *guard, Context::genesis().empty_str.to_owned()); | ||||||||||||||||||||||
| out.push(old_name.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if let Some(mut guard) = self.qualname.try_lock() { | ||||||||||||||||||||||
| let old_qualname = | ||||||||||||||||||||||
| std::mem::replace(&mut *guard, Context::genesis().empty_str.to_owned()); | ||||||||||||||||||||||
| out.push(old_qualname.into()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Note: globals, builtins, code are NOT cleared (required to be non-NULL) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Require
traversewhenclearis enabled.If
clearis set withouttraverse,HAS_TRAVERSEstays false and GC traversal becomes a no-op.🔧 Proposed fix
let has_traverse = class_meta.inner()._has_key("traverse")?; let has_clear = if class_meta.inner()._has_key("clear")? { // If clear attribute is present, use its value class_meta.inner()._bool("clear")? } else { // If clear attribute is absent, default to same as traverse has_traverse }; +if has_clear && !has_traverse { + bail_span!( + item, + "`clear` requires `traverse` (use `traverse` or `traverse = \"manual\"`)" + ); +}🤖 Prompt for AI Agents