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

Break circularity in TypeBounds with LazyRef wraps #14288

Closed
wants to merge 1 commit into from

Conversation

dwijnand
Copy link
Member

No description provided.

@dwijnand dwijnand linked an issue Jan 17, 2022 that may be closed by this pull request
@dwijnand
Copy link
Member Author

@smarter do you have any better ideas on how to avoid the circular problem here?

@smarter
Copy link
Member

smarter commented Jan 18, 2022

What are we recursing on here that leads to a cycle exactly?

@dwijnand
Copy link
Member Author

enum Foo[+H[_]]:
  case Bar[F[_]](f: Foo[F]) extends Foo[F]
  case Baz()

  def test: Foo[H] = this match
    case Bar(Bar(f)) => Bar(f)
    case _           => this

It's a case of TypeBounds with circular infos:

avoid F$1 #18787  >: F$2 <: F
avoid F$2 #18891  <: F$1
avoid F$1 #18787  >: F$2 <: F
avoid F$2 #18891  <: F$1

@smarter
Copy link
Member

smarter commented Jan 18, 2022

Seems weird. I don't even see how we can construct TypeBounds with circular info, I assume there are actually type variables in the info somewhere? (print without using .show/i"..." to see the types that are actually involved)

@smarter
Copy link
Member

smarter commented Jan 18, 2022

If type variables turn out to be involved, this is likely caused by the handling of TypeVar in avoid (https://github.com/lampepfl/dotty/blob/9e14f5fba1acd993b8fcd99e4afe64fc17d37d30/compiler/src/dotty/tools/dotc/core/TypeOps.scala#L508-L512) which maps a type variable to one of its bound without actually instantiating the type variable which I always found weird.

@dwijnand
Copy link
Member Author

Nope, no variables, just type refs:

  • F$1 >: F$2 <: H is TypeRef(NoPrefix,type F$1) with bounds TypeRef(NoPrefix,type F$2) and TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class <empty>)),class Foo)),type H)), and
  • F$2 <: F$1 is TypeRef(NoPrefix,type F$2) with upperbound TypeRef(NoPrefix,type F$1))

@smarter
Copy link
Member

smarter commented Jan 19, 2022

Sounds like a problem with the gadt logic that generates these types then?

@dwijnand
Copy link
Member Author

Oh yeah? Why, what should they be instead? I thought the types seemed legitimate, just tricky to traverse.

@smarter
Copy link
Member

smarter commented Jan 19, 2022

I don't have a good enough grasp of the gadt logic to say, but as far as I can tell no code outside of it can produce type bounds like that so it's natural that the compiler isn't setup to deal with them (and there might be many more type maps where this would be a problem):

scala> def foo[A >: B, B <: A] = {}
-- Error:
1 |def foo[A >: B, B <: A] = {}
  |        ^
  |illegal cyclic type reference: lower bound B of type A refers back to the type itself

scala> class X { type A >: B; type B <: A }
-- Error:
1 |class X { type A >: B; type B <: A }
  |               ^
  |illegal cyclic type reference: lower bound X.this.B of type A refers back to the type itself

@odersky
Copy link
Contributor

odersky commented Jan 23, 2022

There should be no circular info in type bounds. Circular upper bounds are allowed (i.e. that's what an F-bound is), but internally they must be protected with a LazyRef. So we should find out how and where these illegal bounds are generated.

@@ -127,7 +127,20 @@ trait ConstraintHandling {
* of some param when comparing types might lead to infinite recursion. Consider `bounds` instead.
*/
def fullBounds(param: TypeParamRef)(using Context): TypeBounds =
nonParamBounds(param).derivedTypeBounds(fullLowerBound(param), fullUpperBound(param))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely still not the correct place to fix this, constraints shouldn't be cyclic (although the assert for this is disabled by default for performance: https://github.com/lampepfl/dotty/blob/3c8200671995ea75b97b493081cc4e51109cfd04/compiler/src/dotty/tools/dotc/config/Config.scala#L21-L24), it's possible the logic in GadtConstraint.scala violates this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll check.

Copy link
Member Author

@dwijnand dwijnand Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling doesn't seem to trigger anything. Either it fails without my "half answer" workaround or it passes with - no check error. Are we sure it's a cycle in the constraints? Need to investigate more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So inference first creates the type refs while typing the patterns (in maximizeType via typedUnApply) as F$1 <: F and F$2 <: F$1. Just a little later runs over the patterns again in indexPattern where it calls fullBounds leading to F$1(param)1 >: F$2 <: F and F$2(param)1 <: F$1.

So is it circular constraints? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, either gadt.fullBounds should not produce cycles like this, or we shouldn't use it here, wdyt @abgruszecki ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I see it, fullLowerBound aren't wrong in their answers. But put together they lead to a mutual reference. So fixing this in fullBounds by inspecting the results seems reasonable to me.

Hopefully I've fixed the test suite now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can this be triggered with non-gadt constraints?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. 🤷🏼‍♂️ But GadtConstraint redefines its fullLowerBound and fullUpperBound so possibly not?

@dwijnand dwijnand changed the title Fix a recursive TypeBound with some half answers Break circularity in TypeBounds with LazyRef wraps Jan 27, 2022
@dwijnand dwijnand requested a review from smarter February 12, 2022 22:35
@dwijnand dwijnand marked this pull request as ready for review February 12, 2022 22:36
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should get in until we actually understand if this is specific to gadt and if it's working around a bug or the correct thing to do.

@smarter smarter assigned abgruszecki and unassigned smarter Feb 12, 2022
@dwijnand dwijnand removed their assignment Feb 12, 2022
@dwijnand
Copy link
Member Author

I'm concerned that those answers might not come readily. Would you be happy to get the fix in even if we don't have those answers in, let's say, a month? I don't know the answers to your questions, I'm sorry. But it's peeving to put effort into fixing a user-reported bug to then the PR blocked like so.

@smarter
Copy link
Member

smarter commented Feb 13, 2022

I'm not comfortable merging code no one understand, and I don't have the time to try to understand it myself, this is why I asked for Alex's input but someone else could fill in too.

@dwijnand
Copy link
Member Author

dwijnand commented Jul 6, 2022

I'm now not happy with this fix either.

@dwijnand dwijnand closed this Jul 6, 2022
@dwijnand dwijnand deleted the half-answers branch July 6, 2022 13:37
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 this pull request may close these issues.

Compiler hang with covariant type constructor in GADT
4 participants