From 1389bb86df39abf9dbe88a568aee4fd538128cf2 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 13 Feb 2023 22:23:12 +0000 Subject: [PATCH 1/5] Avoid bidirectional GADT typebounds from fullBounds --- .../tools/dotc/core/GadtConstraint.scala | 22 +++++++- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/pos/i14287.min.scala | 7 +++ tests/pos/i14287.scala | 16 ++++++ tests/pos/i15523.avoid.scala | 8 +++ tests/pos/i15523.scala | 7 +++ tests/pos/i16777.scala | 52 +++++++++++++++++++ 7 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 tests/pos/i14287.min.scala create mode 100644 tests/pos/i14287.scala create mode 100644 tests/pos/i15523.avoid.scala create mode 100644 tests/pos/i15523.scala create mode 100644 tests/pos/i16777.scala diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index a863a982a44d..211c8e637b1f 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -71,12 +71,26 @@ class GadtConstraint private ( externalize(constraint.nonParamBounds(param)).bounds def fullLowerBound(param: TypeParamRef)(using Context): Type = - constraint.minLower(param).foldLeft(nonParamBounds(param).lo) { + val self = externalize(param) + constraint.minLower(param).filterNot { p => + val sym = paramSymbol(p) + sym.name.is(NameKinds.UniqueName) && { + val hi = sym.info.hiBound + !hi.isExactlyAny && self <:< hi + } + }.foldLeft(nonParamBounds(param).lo) { (t, u) => t | externalize(u) } def fullUpperBound(param: TypeParamRef)(using Context): Type = - constraint.minUpper(param).foldLeft(nonParamBounds(param).hi) { (t, u) => + val self = externalize(param) + constraint.minUpper(param).filterNot { p => + val sym = paramSymbol(p) + sym.name.is(NameKinds.UniqueName) && { + val lo = sym.info.loBound + !lo.isExactlyNothing && lo <:< self + } + }.foldLeft(nonParamBounds(param).hi) { (t, u) => val eu = externalize(u) // Any as the upper bound means "no bound", but if F is higher-kinded, // Any & F = F[_]; this is wrong for us so we need to short-circuit @@ -96,6 +110,10 @@ class GadtConstraint private ( def tvarOrError(sym: Symbol)(using Context): TypeVar = mapping(sym).ensuring(_ != null, i"not a constrainable symbol: $sym").uncheckedNN + private def paramSymbol(p: TypeParamRef): Symbol = reverseMapping(p) match + case sym: Symbol => sym + case null => NoSymbol + @tailrec final def stripInternalTypeVar(tp: Type): Type = tp match case tv: TypeVar => val inst = constraint.instType(tv) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a87d6dd7e703..328039fd8a41 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1765,7 +1765,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else report.error(new DuplicateBind(b, cdef), b.srcPos) if (!ctx.isAfterTyper) { val bounds = ctx.gadt.fullBounds(sym) - if (bounds != null) sym.info = bounds + if (bounds != null) sym.info = checkNonCyclic(sym, bounds, reportErrors = true) } b case t: UnApply if t.symbol.is(Inline) => Inlines.inlinedUnapply(t) diff --git a/tests/pos/i14287.min.scala b/tests/pos/i14287.min.scala new file mode 100644 index 000000000000..8f7773ab0ac8 --- /dev/null +++ b/tests/pos/i14287.min.scala @@ -0,0 +1,7 @@ +enum Foo[+F[_]]: + case Bar[B[_]](value: Foo[B]) extends Foo[B] + +class Test: + def test[X[_]](foo: Foo[X]): Foo[X] = foo match + case Foo.Bar(Foo.Bar(x)) => Foo.Bar(x) + case _ => foo diff --git a/tests/pos/i14287.scala b/tests/pos/i14287.scala new file mode 100644 index 000000000000..1291dc8adefc --- /dev/null +++ b/tests/pos/i14287.scala @@ -0,0 +1,16 @@ +// scalac: -Yno-deep-subtypes +enum Free[+F[_], A]: + case Return(a: A) + case Suspend(s: F[A]) + case FlatMap[F[_], A, B]( + s: Free[F, A], + f: A => Free[F, B]) extends Free[F, B] + + def flatMap[F2[x] >: F[x], B](f: A => Free[F2,B]): Free[F2,B] = + FlatMap(this, f) + + @scala.annotation.tailrec + final def step: Free[F, A] = this match + case FlatMap(FlatMap(fx, f), g) => fx.flatMap(x => f(x).flatMap(y => g(y))).step + case FlatMap(Return(x), f) => f(x).step + case _ => this diff --git a/tests/pos/i15523.avoid.scala b/tests/pos/i15523.avoid.scala new file mode 100644 index 000000000000..afbfc1a69d60 --- /dev/null +++ b/tests/pos/i15523.avoid.scala @@ -0,0 +1,8 @@ +// scalac: -Werror +// like the original, but with a case body `a` +// which caused type avoidance to infinitely recurse +sealed trait Parent +final case class Leaf[A, B >: A](a: A, b: B) extends Parent + +def run(x: Parent) = x match + case Leaf(a, _) => a diff --git a/tests/pos/i15523.scala b/tests/pos/i15523.scala new file mode 100644 index 000000000000..cf63613c29ac --- /dev/null +++ b/tests/pos/i15523.scala @@ -0,0 +1,7 @@ +// scalac: -Werror +sealed trait Parent +final case class Leaf[A, B >: A](a: A, b: B) extends Parent + +def run(x: Parent): Unit = x match { + case Leaf(a, b) => +} diff --git a/tests/pos/i16777.scala b/tests/pos/i16777.scala new file mode 100644 index 000000000000..4218aea29d9f --- /dev/null +++ b/tests/pos/i16777.scala @@ -0,0 +1,52 @@ +// scalac: -Ykind-projector:underscores + +sealed abstract class Free[+S[_, _], +E, +A] { + @inline final def flatMap[S1[e, a] >: S[e, a], B, E1 >: E](fun: A => Free[S1, E1, B]): Free[S1, E1, B] = Free.FlatMapped[S1, E, E1, A, B](this, fun) + @inline final def map[B](fun: A => B): Free[S, E, B] = flatMap(a => Free.pure[S, B](fun(a))) + @inline final def as[B](as: => B): Free[S, E, B] = map(_ => as) + @inline final def *>[S1[e, a] >: S[e, a], B, E1 >: E](sc: Free[S1, E1, B]): Free[S1, E1, B] = flatMap(_ => sc) + @inline final def <*[S1[e, a] >: S[e, a], B, E1 >: E](sc: Free[S1, E1, B]): Free[S1, E1, A] = flatMap(r => sc.as(r)) + + @inline final def void: Free[S, E, Unit] = map(_ => ()) + + // FIXME: Scala 3.1.4 bug: false unexhaustive match warning + /// @nowarn("msg=pattern case: Free.FlatMapped") + @inline final def foldMap[S1[e, a] >: S[e, a], G[+_, +_]](transform: S1 ~>> G)(implicit G: Monad2[G]): G[E, A] = { + this match { + case Free.Pure(a) => G.pure(a) + case Free.Suspend(a) => transform.apply(a) + case Free.FlatMapped(sub, cont) => + sub match { + case Free.FlatMapped(sub2, cont2) => sub2.flatMap(a => cont2(a).flatMap(cont)).foldMap(transform) + case another => G.flatMap(another.foldMap(transform))(cont(_).foldMap(transform)) + } + } + } +} + +trait ~>>[-F[_, _], +G[_, _]] { + def apply[E, A](f: F[E, A]): G[E, A] +} + +object Free { + @inline def pure[S[_, _], A](a: A): Free[S, Nothing, A] = Pure(a) + @inline def lift[S[_, _], E, A](s: S[E, A]): Free[S, E, A] = Suspend(s) + + final case class Pure[S[_, _], A](a: A) extends Free[S, Nothing, A] { + override def toString: String = s"Pure:[$a]" + } + final case class Suspend[S[_, _], E, A](a: S[E, A]) extends Free[S, E, A] { + override def toString: String = s"Suspend:[$a]" + } + final case class FlatMapped[S[_, _], E, E1 >: E, A, B](sub: Free[S, E, A], cont: A => Free[S, E1, B]) extends Free[S, E1, B] { + override def toString: String = s"FlatMapped:[sub=$sub]" + } +} + +type Monad2[F[+_, +_]] = Monad3[λ[(`-R`, `+E`, `+A`) => F[E, A]]] + +trait Monad3[F[-_, +_, +_]] { + def flatMap[R, E, A, B](r: F[R, E, A])(f: A => F[R, E, B]): F[R, E, B] + def flatten[R, E, A](r: F[R, E, F[R, E, A]]): F[R, E, A] = flatMap(r)(identity) + def pure[A](a: A): F[Any, Nothing, A] +} From 715bca84aa9787be6bd07502795afb845ce2aa5c Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 14 Feb 2023 13:26:11 +0000 Subject: [PATCH 2/5] Drop failsafe checkNonCyclic and document GADT fullBounds change --- compiler/src/dotty/tools/dotc/core/GadtConstraint.scala | 9 ++++++++- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 211c8e637b1f..4b580c06d11f 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -77,6 +77,13 @@ class GadtConstraint private ( sym.name.is(NameKinds.UniqueName) && { val hi = sym.info.hiBound !hi.isExactlyAny && self <:< hi + // drop any lower param that is a GADT symbol + // and is upper-bounded by a non-Any super-type of the original parameter + // e.g. in pos/i14287.min + // B$1 had info <: X and fullBounds >: B$2 <: X, and + // B$2 had info <: B$1 and fullBounds <: B$1 + // We can use the info of B$2 to drop the lower-bound of B$1 + // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. } }.foldLeft(nonParamBounds(param).lo) { (t, u) => t | externalize(u) @@ -88,7 +95,7 @@ class GadtConstraint private ( val sym = paramSymbol(p) sym.name.is(NameKinds.UniqueName) && { val lo = sym.info.loBound - !lo.isExactlyNothing && lo <:< self + !lo.isExactlyNothing && lo <:< self // same as fullLowerBounds } }.foldLeft(nonParamBounds(param).hi) { (t, u) => val eu = externalize(u) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 328039fd8a41..a87d6dd7e703 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1765,7 +1765,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else report.error(new DuplicateBind(b, cdef), b.srcPos) if (!ctx.isAfterTyper) { val bounds = ctx.gadt.fullBounds(sym) - if (bounds != null) sym.info = checkNonCyclic(sym, bounds, reportErrors = true) + if (bounds != null) sym.info = bounds } b case t: UnApply if t.symbol.is(Inline) => Inlines.inlinedUnapply(t) From 22be8026245da199ea37d7721e861036de4834bd Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 15 Feb 2023 21:39:51 +0000 Subject: [PATCH 3/5] GADT: move dropping GADT symbols into foldLeft --- .../tools/dotc/core/GadtConstraint.scala | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 4b580c06d11f..c98445c61a7e 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -3,6 +3,7 @@ package dotc package core import Contexts.*, Decorators.*, Symbols.*, Types.* +import NameKinds.UniqueName import config.Printers.{gadts, gadtsConstr} import util.{SimpleIdentitySet, SimpleIdentityMap} import printing._ @@ -72,36 +73,30 @@ class GadtConstraint private ( def fullLowerBound(param: TypeParamRef)(using Context): Type = val self = externalize(param) - constraint.minLower(param).filterNot { p => - val sym = paramSymbol(p) - sym.name.is(NameKinds.UniqueName) && { - val hi = sym.info.hiBound - !hi.isExactlyAny && self <:< hi - // drop any lower param that is a GADT symbol - // and is upper-bounded by a non-Any super-type of the original parameter - // e.g. in pos/i14287.min - // B$1 had info <: X and fullBounds >: B$2 <: X, and - // B$2 had info <: B$1 and fullBounds <: B$1 - // We can use the info of B$2 to drop the lower-bound of B$1 - // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. - } - }.foldLeft(nonParamBounds(param).lo) { - (t, u) => t | externalize(u) + constraint.minLower(param).foldLeft(nonParamBounds(param).lo) { (acc, p) => + externalize(p) match + case tp: TypeRef + // drop any lower param that is a GADT symbol + // and is upper-bounded by a non-Any super-type of the original parameter + // e.g. in pos/i14287.min + // B$1 had info <: X and fullBounds >: B$2 <: X, and + // B$2 had info <: B$1 and fullBounds <: B$1 + // We can use the info of B$2 to drop the lower-bound of B$1 + // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. + if tp.name.is(UniqueName) && !tp.info.hiBound.isExactlyAny && self <:< tp.info.hiBound => acc + case tp => acc | tp } def fullUpperBound(param: TypeParamRef)(using Context): Type = val self = externalize(param) - constraint.minUpper(param).filterNot { p => - val sym = paramSymbol(p) - sym.name.is(NameKinds.UniqueName) && { - val lo = sym.info.loBound - !lo.isExactlyNothing && lo <:< self // same as fullLowerBounds - } - }.foldLeft(nonParamBounds(param).hi) { (t, u) => - val eu = externalize(u) - // Any as the upper bound means "no bound", but if F is higher-kinded, - // Any & F = F[_]; this is wrong for us so we need to short-circuit - if t.isAny then eu else t & eu + constraint.minUpper(param).foldLeft(nonParamBounds(param).hi) { (acc, u) => + externalize(u) match + case tp: TypeRef // same as fullLowerBounds + if tp.name.is(UniqueName) && !tp.info.loBound.isExactlyNothing && tp.info.loBound <:< self => acc + case tp => + // Any as the upper bound means "no bound", but if F is higher-kinded, + // Any & F = F[_]; this is wrong for us so we need to short-circuit + if acc.isAny then tp else acc & tp } def externalize(tp: Type, theMap: TypeMap | Null = null)(using Context): Type = tp match @@ -117,10 +112,6 @@ class GadtConstraint private ( def tvarOrError(sym: Symbol)(using Context): TypeVar = mapping(sym).ensuring(_ != null, i"not a constrainable symbol: $sym").uncheckedNN - private def paramSymbol(p: TypeParamRef): Symbol = reverseMapping(p) match - case sym: Symbol => sym - case null => NoSymbol - @tailrec final def stripInternalTypeVar(tp: Type): Type = tp match case tv: TypeVar => val inst = constraint.instType(tv) From 86d89379e5185dab1ff6978f8caf67f24955a259 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 15 Feb 2023 22:25:14 +0000 Subject: [PATCH 4/5] GADT: Use isPatternBound, ofc... --- compiler/src/dotty/tools/dotc/core/GadtConstraint.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index c98445c61a7e..74d668e7ca87 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -83,7 +83,7 @@ class GadtConstraint private ( // B$2 had info <: B$1 and fullBounds <: B$1 // We can use the info of B$2 to drop the lower-bound of B$1 // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. - if tp.name.is(UniqueName) && !tp.info.hiBound.isExactlyAny && self <:< tp.info.hiBound => acc + if tp.symbol.isPatternBound && !tp.info.hiBound.isExactlyAny && self <:< tp.info.hiBound => acc case tp => acc | tp } @@ -92,7 +92,7 @@ class GadtConstraint private ( constraint.minUpper(param).foldLeft(nonParamBounds(param).hi) { (acc, u) => externalize(u) match case tp: TypeRef // same as fullLowerBounds - if tp.name.is(UniqueName) && !tp.info.loBound.isExactlyNothing && tp.info.loBound <:< self => acc + if tp.symbol.isPatternBound && !tp.info.loBound.isExactlyNothing && tp.info.loBound <:< self => acc case tp => // Any as the upper bound means "no bound", but if F is higher-kinded, // Any & F = F[_]; this is wrong for us so we need to short-circuit From 37aeb3d667a9e77fcfbb78c30112419acc01df9c Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 15 Feb 2023 22:30:09 +0000 Subject: [PATCH 5/5] GADT: Use =:= instead of Any/Nothing --- .../tools/dotc/core/GadtConstraint.scala | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 74d668e7ca87..bb65cce84042 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -75,15 +75,14 @@ class GadtConstraint private ( val self = externalize(param) constraint.minLower(param).foldLeft(nonParamBounds(param).lo) { (acc, p) => externalize(p) match - case tp: TypeRef - // drop any lower param that is a GADT symbol - // and is upper-bounded by a non-Any super-type of the original parameter - // e.g. in pos/i14287.min - // B$1 had info <: X and fullBounds >: B$2 <: X, and - // B$2 had info <: B$1 and fullBounds <: B$1 - // We can use the info of B$2 to drop the lower-bound of B$1 - // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. - if tp.symbol.isPatternBound && !tp.info.hiBound.isExactlyAny && self <:< tp.info.hiBound => acc + // drop any lower param that is a GADT symbol + // and is upper-bounded by a non-Any super-type of the original parameter + // e.g. in pos/i14287.min + // B$1 had info <: X and fullBounds >: B$2 <: X, and + // B$2 had info <: B$1 and fullBounds <: B$1 + // We can use the info of B$2 to drop the lower-bound of B$1 + // and return non-bidirectional bounds B$1 <: X and B$2 <: B$1. + case tp: TypeRef if tp.symbol.isPatternBound && self =:= tp.info.hiBound => acc case tp => acc | tp } @@ -91,8 +90,7 @@ class GadtConstraint private ( val self = externalize(param) constraint.minUpper(param).foldLeft(nonParamBounds(param).hi) { (acc, u) => externalize(u) match - case tp: TypeRef // same as fullLowerBounds - if tp.symbol.isPatternBound && !tp.info.loBound.isExactlyNothing && tp.info.loBound <:< self => acc + case tp: TypeRef if tp.symbol.isPatternBound && self =:= tp.info.loBound => acc // like fullLowerBound case tp => // Any as the upper bound means "no bound", but if F is higher-kinded, // Any & F = F[_]; this is wrong for us so we need to short-circuit