Skip to content
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

Generics - recursive bound results in UnresolvedTypeVariable #88

Closed
mkouba opened this issue Sep 18, 2020 · 14 comments · Fixed by #221
Closed

Generics - recursive bound results in UnresolvedTypeVariable #88

mkouba opened this issue Sep 18, 2020 · 14 comments · Fixed by #221
Assignees
Milestone

Comments

@mkouba
Copy link
Contributor

mkouba commented Sep 18, 2020

If a class declares a recursive bound (which should be legal and often used e.g. in builders) such as interface Score<S extends Score<S>> {} the type hierarchy contains an UnresolvedTypeVariable. And according to the javadoc "This type will only occur as a result of a bug, or a non-compliant Java class file".

A test can be found here:
mkouba@2141db3

In fact, I'm not quite sure whether it's a bug or how to handle this correctly.

@mkouba
Copy link
Contributor Author

mkouba commented Sep 18, 2020

CC @n1hility

@ge0ffrey
Copy link

ge0ffrey commented Sep 18, 2020

This bug blocks an improvement in the OptaPlanner quarkus extension to put it feature wise on par with the spring-boot starter.
https://github.com/kiegroup/optaplanner/blob/master/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/OptaPlannerBeanProvider.java#L59

@n1hility
Copy link
Contributor

recursive types should be allowed, so I am calling this a bug. The unresolved type is for cases where you have undefined type parameters which at one point or another some bytecode frameworks were generating.

@n1hility n1hility self-assigned this Sep 18, 2020
@ge0ffrey
Copy link

Is there a workaround for this issue?

@n1hility
Copy link
Contributor

@ge0ffrey the only workaround at the moment is to do this on the caller side by tracking identifiers.

Sorry this one is taking longer for me to get to. There are some fundamental problems with implementing a solution that are tricky to get right. There are two major issues at play.

The first is that we don't want to introduce a non-terminating hang (or stack overflow) for existing code out there that is currently expecting a finite scan. So we need to retrofit the API in some way to address that.

The second issue is that the internals of jandex are designed to recycle instances for memory usage due to the amount of state involved (for example the exact same method signature, or generic type definition in multiple places in the index (different classes etc is reused via an intern pool). This requires immutable acyclic type nodes, and in the process of analysis these are rebuilt in a copy on write fashion. Altogether this makes it difficult to represent cyclical references and in a nice clean API.

Solving this will likely involve some form of external cycle linkage table with unique identifiers (in a way that doesn't bloat memory) and probably a new Type like (e.g. CyclicTypeVariable)

.

@Felk
Copy link

Felk commented May 12, 2021

Hello, is there an ETA on when this would likely be fixed? This issue is unfortunately blocking me right now and I'd need to know whether it's better to wait for a fix or to start searching for an alternative.

EDIT: I avoided the issue by not having the class I rely on be discovered through CDI, but to specialize the class that injects it and hardcode it there instead.

@n1hility
Copy link
Contributor

@Felk Glad you found a workaround. I am sorry for the long delay on this issue. It requires basically a rewrite of the generics handling, enforced inner class ordering, and additional APIs. Due to the garage scope I have prioritized the other items for 3.0. I will prioritize this for a 3.1 immediately after.

@Ladicek Ladicek assigned Ladicek and unassigned n1hility May 31, 2022
@Ladicek
Copy link
Contributor

Ladicek commented May 31, 2022

Assigning to myself, as this is the last thing I wanna do for Jandex 3.0.

@Ladicek
Copy link
Contributor

Ladicek commented Jun 3, 2022

I think I have a pretty decent implementation of this. It adds a new kind of type, a TypeVariableReference, which is used in a class/method list of type parameters to represent a type variable that wasn't fully defined yet. For simple recursive type parameters, such as T extends Comparable<T>, this naturally means that type variables occurring in their own definitions are represented as references. For mutually recursive type parameters, type variables that occur in their definitions or before their definitions are represented as references. A type variable reference can be follow()-ed to obtain the underlying type variable.

Implementation-wise, this requires a patching pass both when indexing a class (to resolve the unresolved type variables and propagate annotated type variables) and when deserializing an index (types are serialized in topological order, that is, if a type is composite, it is serialized after its constituent types, and references go against that), but nothing special.

I'm currently finishing the work to support type annotations on mutually recursive type parameters, everything else is done, so hopefully next week.

@n1hility
Copy link
Contributor

n1hility commented Jun 3, 2022

@Ladicek awesome, sounds like a good approach. Did you look at the interplay with type annotations? When those are applied they have to replace an immutable reference tree and this approach will conflict with cycles, switching to a reference type indirection as you are doing should solve it.

@Ladicek
Copy link
Contributor

Ladicek commented Jun 4, 2022

The approach for type annotations I have is pretty sensible I think: for @Ann1 T extends Comparable<@Ann2 T>, the 2nd T is represented as a reference which has the @Ann2 type annotation. When the reference is followed, you get the type variable for 1st T, which has the @Ann1 type annotation. The same holds for mutually recursive type parameters, though it requires a little more complexity implementation-wise.

@Ladicek
Copy link
Contributor

Ladicek commented Jun 10, 2022

OK so type annotations on mutually recursive type parameters are quite a bit more complex than I imagined. Basically, when I have

class Graph<G extends Graph<G, E, V>, E extends Edge<G, E, V>, V extends Vertex<G, E, V>> {
}

then the bound of G extends Graph<G, E, V> contains 3 type variable references (G, E, V), but the bound of E extends Edge<G, E, V> contains 1 type variable (G) and 2 type variable references (E, V), and the bound of V extends Vertex<G, E, V> contains 2 type variables (G, E) and 1 type variable reference (V).

If I add type annotations to the type variable occurences, they are not propagated correctly to the "nested" type variables, because those are different instances. Which probably happens due to the copy-on-write mechanism of attaching type annotations to types.

I'll need to spend more time on this.

@Ladicek
Copy link
Contributor

Ladicek commented Jun 20, 2022

I believe I finally nailed it: #221 I want to do some tests with parameterized types that have an owner, but other than that, I think I'm done. Yay!

@Ladicek
Copy link
Contributor

Ladicek commented Jun 21, 2022

Done in #221.

@Ladicek Ladicek closed this as completed Jun 21, 2022
@Ladicek Ladicek added this to the 3.0.0 milestone Jun 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants