kyren / gc-arena

Incremental garbage collection from safe Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Build Hangs Recursive Struct

dragazo opened this issue · comments

I've been using this crate extensively for a no-std language vm. Part of my interface is as follows (minimized):

#[derive(Collect)]
#[collect(no_drop)]
pub enum Value<'gc> {
    List(Gc<'gc, Vec<Value<'gc>>>),
    Closure(Gc<'gc, Closure<'gc>>),
}

#[derive(Collect)]
#[collect(no_drop)]
pub struct Closure<'gc> {
    pub vars: Vec<Value<'gc>>,
}

This has worked perfectly fine for months. However, I recently made one little change to add a custom user type S, and suddenly cargo build and cargo check hang and eat up memory until it runs out and crashes. I've managed to track down the issue to this dependency and have a minimal example:

#[derive(Collect)]
#[collect(no_drop)]
pub enum Value<'gc, S> { // note the param S, which seems to cause the problem
    List(Gc<'gc, Vec<Value<'gc, S>>>),
    Closure(Gc<'gc, Closure<'gc, S>>),
}

#[derive(Collect)]
#[collect(no_drop)]
pub struct Closure<'gc, S> {
    pub vars: Vec<Value<'gc, S>>,
}

Worse, there is no error message whatsoever, so it was hard to track down. But I saw #4 and decided to try it. In my minimal example, this now gives a recursion depth error (better), but it doesn't solve my actual code base issues, which are more like the following again hanging example:

use gc_arena::{Collect, Gc};

#[derive(Collect)]
#[collect(no_drop)]
pub enum Value<'gc, S> {
    List(Gc<'gc, Vec<Self>>),
    Closure(Gc<'gc, Closure<'gc, S>>),
    Closure2(Gc<'gc, Closure2<'gc, S>>),
}

#[derive(Collect)]
#[collect(no_drop)]
pub struct Closure<'gc, S> {
    pub vars: Vec<Value<'gc, S>>,
}

#[derive(Collect)]
#[collect(no_drop)]
pub struct Closure2<'gc, S> {
    pub vars: Vec<Value<'gc, S>>,
}

Because adding a second non-self branch to the enum reintroduces the problem, I'm assuming this crate does some kind of breadth-first search for checking that a type's fields/variants satisfy Collect requirements (with a special case for Self), but which doesn't protect from cycles? Is there not a better way to check for cycles than a special case for (only) Self?

I need to be able to expose Closure instances to the library user, but I could possibly fix this trivial example by inlining the fields of Closure in Value::Closure, replacing Value<'gc, S> with Self, and reconstructing the actual Closure instance from the fields when requested by the user. However this would not solve my actual issues because the real (full) definition of Closure has several layers of indirect typing, all of which need to be exposed to users.

So my main questions are: why does the code without S not cause a problem, and is there some workaround to make this work?

This isn't a good solution, but just as a temporary measure you can always unsafely implement Collect manually.

I don't know what's causing this error, but IF you have time and want to look into it, the first question I would want answered is whether it's breaking during the proc macro or during compilation. I assume like #4 it's happening during compilation? If so, if you can output whatever the proc macro is generating that would be helpful.

This crate is using synstructure to do pretty much everything complicated in its proc macro Collect derive, and there's not really much going on outside of that. I suspect that the where bounds on fields (which we tell syntstructure to generate here) are very weird and its the compilation that is hanging? It's gross but would it work to just not have bounds on the field types and just let the compilation fail later? Does this derive work if you change AddBounds::Fields in that line to AddBounds::None?

Right, it looks like it's breaking at compilation, cause cargo expand terminates even when cargo check doesn't. I think I've found a fix in the gc-arena crate: just changing the T: 'gc + Collect trait bound to T: 'gc in the definition of the Gc and GcCell structs fixes the problem, and I can make a PR for this. I think this would be safe to allow, because all instances of Gc and GcCell are created by functions that require the full T: 'gc + Collect bound, so removing the Collect requirement from the struct definitions doesn't (I don't think) introduce any unsoundness. Indeed, because the struct itself required that trait bound, we already know all references to it for any operation already require the full bounds, so it shouldn't change anything, but fixes the infinite recursion error during trait bound evaluation.

That's a neat way to solve that problem! I was just coming here actually to also suggest that we might need something like this to cover all cases: https://serde.rs/container-attrs.html#bound

I'm pretty sure your PR is correct independent of this, but I still think you can probably tie the compiler in knots somehow. I think a more complete solution to this problem might be to copy what serde does, so add bounds based on the generic type parameters, not the fields, and provide a #[collect(bound = "")] escape hatch... if I'm understanding all this correctly which I'm not super confident about still.

In any case, I think that PR makes everything easier to deal with and prevents unnecessary bounds so it's maybe a good idea anyway?