Skip to content

Commit 7f6a5e8

Browse files
committed
[derive] Handle cycles in Trace::NEEDS_TRACE
1 parent 5cdf522 commit 7f6a5e8

File tree

2 files changed

+80
-8
lines changed

2 files changed

+80
-8
lines changed

libs/derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ serde = { version = "1" }
1717

1818
[dependencies]
1919
# Proc macros
20-
syn = { version = "1.0.55", features = ["full", "extra-traits", "fold"] }
20+
syn = { version = "1.0.55", features = ["full", "extra-traits", "visit", "fold"] }
2121
quote = "1.0.8"
2222
darling = "0.13"
2323
proc-macro2 = "1"

libs/derive/src/derive.rs

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,43 @@ struct TraceField {
189189
/// to be traced.
190190
#[darling(default)]
191191
unsafe_skip_trace: bool,
192+
/// Avoid cycles in the computation of 'Trace::NEEDS_TRACE'.
193+
///
194+
/// This overrides the default value of the 'Trace::NEEDS_TRACE'.
195+
/// It may be necessary to avoid cycles in const evaluation.
196+
/// For example,
197+
/// ```no_run
198+
/// # use zerogc_derive::Trace;
199+
/// #[derive(Trace)]
200+
/// struct FooIndirect(Foo);
201+
/// #[derive(Trace)]
202+
/// struct Foo {
203+
/// #[zerogc(avoid_const_cycle)]
204+
/// foo: Option<Box<FooIndirect>>
205+
/// }
206+
/// ```
207+
/// the default generated value of 'Trace::NEEDS_TRACE'
208+
/// would be equal to 'const NEEDS_TRACE = Option<Box<FooIndirect>>::NEEDS_TRACE'.
209+
/// This would cause an infinite cycle, leading to a compiler error.
210+
///
211+
/// NOTE: The macro has builtin cycle-protection for `Box<Foo>`.
212+
/// It's only the existence of 'FooIndirect' that causes a problem.
213+
///
214+
/// For example, the following works fine without an explicit attribute:
215+
/// ```no_run
216+
/// # use zerogc_derive::Trace;
217+
/// #[derive(Trace)]
218+
/// // NOTE: No explicit attribute needed ;)
219+
/// struct Foo {
220+
/// foo: Box<Foo>
221+
/// }
222+
/// ```
223+
///
224+
/// If this is false, it overrides and disables the automatic cycle detection.
225+
///
226+
/// Both options are completely safe.
227+
#[darling(default)]
228+
avoid_const_cycle: Option<bool>,
192229
#[darling(default, rename = "serde")]
193230
serde_opts: Option<SerdeFieldOpts>,
194231
#[darling(forward_attrs(serde))]
@@ -906,17 +943,32 @@ impl TraceDeriveInput {
906943
} else {
907944
quote!(false)
908945
};
909-
let traced_field_types = self.determine_field_types(false);
910-
let all_field_types = self.determine_field_types(true);
911-
let needs_trace = traced_field_types.iter().map(|ty| quote_spanned!(ty.span() => <#ty as zerogc::Trace>::NEEDS_TRACE));
946+
let all_fields = self.all_fields();
947+
let needs_trace = all_fields.iter()
948+
.filter(|field| !field.unsafe_skip_trace)
949+
.map(|field| {
950+
let avoid_cycle = field.avoid_const_cycle.unwrap_or_else(|| {
951+
detect_cycle(&field.ty, target_type.clone())
952+
});
953+
let ty = &field.ty;
954+
if avoid_cycle {
955+
quote!(true)
956+
} else {
957+
quote_spanned!(ty.span() => <#ty as zerogc::Trace>::NEEDS_TRACE)
958+
}
959+
});
912960
let needs_drop = if self.is_copy {
913961
vec![quote!(false)]
914962
} else {
915-
all_field_types.iter().map(|ty| {
916-
if traced_field_types.contains(ty) {
917-
quote_spanned!(ty.span() => <#ty as zerogc::Trace>::NEEDS_DROP)
918-
} else {
963+
all_fields.iter().map(|field| {
964+
let avoid_cycle = field.avoid_const_cycle.unwrap_or_else(|| {
965+
detect_cycle(&field.ty, target_type.clone())
966+
});
967+
let ty = &field.ty;
968+
if field.unsafe_skip_trace || avoid_cycle {
919969
quote_spanned!(ty.span() => core::mem::needs_drop::<#ty>())
970+
} else {
971+
quote_spanned!(ty.span() => <#ty as zerogc::Trace>::NEEDS_DROP)
920972
}
921973
}).collect::<Vec<_>>()
922974
};
@@ -1132,6 +1184,26 @@ impl TraceDeriveInput {
11321184
}
11331185
}
11341186

1187+
fn detect_cycle(target: &syn::Type, potential_cycle: impl Into<syn::Path>) -> bool {
1188+
struct CycleDetector {
1189+
potential_cycle: Path,
1190+
found: bool
1191+
}
1192+
impl<'ast> syn::visit::Visit<'ast> for CycleDetector {
1193+
fn visit_path(&mut self, path: &'ast Path) {
1194+
if *path == self.potential_cycle {
1195+
self.found = true;
1196+
}
1197+
syn::visit::visit_path(self, path);
1198+
}
1199+
}
1200+
let mut visitor = CycleDetector {
1201+
potential_cycle: potential_cycle.into(),
1202+
found: false
1203+
};
1204+
syn::visit::visit_type(&mut visitor, target);
1205+
visitor.found
1206+
}
11351207
pub enum FieldAccess {
11361208
None,
11371209
SelfMember {

0 commit comments

Comments
 (0)