From 5eb0716487516d632480050dee077f10f9e29033 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Aug 2025 19:44:59 +0200 Subject: [PATCH 1/4] More careful ClassTag instantiation We now use a blend of the new scheme and backwards compatible special case if type variables as ClassTag arguments are constrained by further type variables. Fixes #23611 --- .../src/dotty/tools/dotc/core/Types.scala | 10 ++++++-- .../dotty/tools/dotc/typer/Synthesizer.scala | 24 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index a6f59d8b2d33..355bea53200b 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5116,11 +5116,17 @@ object Types extends TypeUtils { */ private def currentEntry(using Context): Type = ctx.typerState.constraint.entry(origin) + /** For uninstantiated type variables: the lower bound */ + def lowerBound(using Context): Type = currentEntry.loBound + + /** For uninstantiated type variables: the upper bound */ + def upperBound(using Context): Type = currentEntry.hiBound + /** For uninstantiated type variables: Is the lower bound different from Nothing? */ - def hasLowerBound(using Context): Boolean = !currentEntry.loBound.isExactlyNothing + def hasLowerBound(using Context): Boolean = !lowerBound.isExactlyNothing /** For uninstantiated type variables: Is the upper bound different from Any? */ - def hasUpperBound(using Context): Boolean = !currentEntry.hiBound.isTopOfSomeKind + def hasUpperBound(using Context): Boolean = !upperBound.isTopOfSomeKind /** Unwrap to instance (if instantiated) or origin (if not), until result * is no longer a TypeVar diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index d1977bb44888..da587d722687 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -38,9 +38,29 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): // bounds are usually widened during instantiation. instArg(tp.tp1) case tvar: TypeVar if ctx.typerState.constraint.contains(tvar) => + // If tvar has a lower or upper bound: + // 1. If the bound is not another type variable, use this as approximation. + // 2. Otherwise, if the type can be forced to be fully defined, use that type + // as approximation. + // 3. Otherwise leave argument uninstantiated. + // The reason for (2) is that we observed complicated constraints in i23611.scala + // that get better types if a fully defined type is computed than if several type + // variables are approximated incrementally. This is a minimization of some ZIP code. + // So in order to keep backwards compatibility (where before we _only_ did 2) we + // add that special case. + def isGroundConstr(tp: Type): Boolean = tp.dealias match + case tvar: TypeVar if ctx.typerState.constraint.contains(tvar) => false + case tp: AndOrType => isGroundConstr(tp.tp1) && isGroundConstr(tp.tp2) + case _ => true instArg( - if tvar.hasLowerBound then tvar.instantiate(fromBelow = true) - else if tvar.hasUpperBound then tvar.instantiate(fromBelow = false) + if tvar.hasLowerBound then + if isGroundConstr(tvar.lowerBound) then tvar.instantiate(fromBelow = true) + else if isFullyDefined(tp, ForceDegree.all) then tp + else NoType + else if tvar.hasUpperBound then + if isGroundConstr(tvar.upperBound) then tvar.instantiate(fromBelow = false) + else if isFullyDefined(tp, ForceDegree.all) then tp + else NoType else NoType) case _ => tp From 4868b8dd4cb5992410de34021c5b5053840bd3f6 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Aug 2025 09:44:34 +0200 Subject: [PATCH 2/4] Update compiler/src/dotty/tools/dotc/typer/Synthesizer.scala --- compiler/src/dotty/tools/dotc/typer/Synthesizer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index da587d722687..aac70834f173 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -45,7 +45,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): // 3. Otherwise leave argument uninstantiated. // The reason for (2) is that we observed complicated constraints in i23611.scala // that get better types if a fully defined type is computed than if several type - // variables are approximated incrementally. This is a minimization of some ZIP code. + // variables are approximated incrementally. This is a minimization of some ZIO code. // So in order to keep backwards compatibility (where before we _only_ did 2) we // add that special case. def isGroundConstr(tp: Type): Boolean = tp.dealias match From 3e6d1bc1d46eee2f3d1f63275ea24448f30fb433 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Aug 2025 10:15:09 +0200 Subject: [PATCH 3/4] Add test case --- tests/pos/i23611.scala | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/pos/i23611.scala diff --git a/tests/pos/i23611.scala b/tests/pos/i23611.scala new file mode 100644 index 000000000000..0fef178b9c32 --- /dev/null +++ b/tests/pos/i23611.scala @@ -0,0 +1,26 @@ +import java.io.{File, IOException} +import java.net.URI +import java.nio.file.{Path, Paths} +import scala.reflect.ClassTag + +trait FileConnectors { + def listPath(path: => Path): ZStream[Any, IOException, Path] + + final def listFile(file: => File): ZStream[Any, IOException, File] = + for { + path <- null.asInstanceOf[ZStream[Any, IOException, Path]] + r <- listPath(path).mapZIO(a => ZIO.attempt(a.toFile).refineToOrDie) + } yield r +} + +sealed trait ZIO[-R, +E, +A] +extension [R, E <: Throwable, A](self: ZIO[R, E, A]) + def refineToOrDie[E1 <: E: ClassTag]: ZIO[R, E1, A] = ??? + +object ZIO: + def attempt[A](code: => A): ZIO[Any, Throwable, A] = ??? + +sealed trait ZStream[-R, +E, +A]: + def map[B](f: A => B): ZStream[R, E, B] = ??? + def flatMap[R1 <: R, E1 >: E, B](f: A => ZStream[R1, E1, B]): ZStream[R1, E1, B] + def mapZIO[R1 <: R, E1 >: E, A1](f: A => ZIO[R1, E1, A1]): ZStream[R1, E1, A1] \ No newline at end of file From 9bb08274ae990830defeefc3b34fb0048a9ba4d7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 6 Aug 2025 11:39:38 +0200 Subject: [PATCH 4/4] Refine criterion when to use fullyDefinedType in ClassTag search --- .../src/dotty/tools/dotc/core/Types.scala | 10 ++----- .../dotty/tools/dotc/typer/Synthesizer.scala | 12 ++++---- tests/pos/i23611a.scala | 30 +++++++++++++++++++ 3 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 tests/pos/i23611a.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 355bea53200b..a6f59d8b2d33 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5116,17 +5116,11 @@ object Types extends TypeUtils { */ private def currentEntry(using Context): Type = ctx.typerState.constraint.entry(origin) - /** For uninstantiated type variables: the lower bound */ - def lowerBound(using Context): Type = currentEntry.loBound - - /** For uninstantiated type variables: the upper bound */ - def upperBound(using Context): Type = currentEntry.hiBound - /** For uninstantiated type variables: Is the lower bound different from Nothing? */ - def hasLowerBound(using Context): Boolean = !lowerBound.isExactlyNothing + def hasLowerBound(using Context): Boolean = !currentEntry.loBound.isExactlyNothing /** For uninstantiated type variables: Is the upper bound different from Any? */ - def hasUpperBound(using Context): Boolean = !upperBound.isTopOfSomeKind + def hasUpperBound(using Context): Boolean = !currentEntry.hiBound.isTopOfSomeKind /** Unwrap to instance (if instantiated) or origin (if not), until result * is no longer a TypeVar diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index aac70834f173..8de28a3274ca 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -20,6 +20,7 @@ import ast.tpd.* import Synthesizer.* import sbt.ExtractDependencies.* import xsbti.api.DependencyContext.* +import TypeComparer.{fullLowerBound, fullUpperBound} /** Synthesize terms for special classes */ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): @@ -50,18 +51,20 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): // add that special case. def isGroundConstr(tp: Type): Boolean = tp.dealias match case tvar: TypeVar if ctx.typerState.constraint.contains(tvar) => false + case pref: TypeParamRef if ctx.typerState.constraint.contains(pref) => false case tp: AndOrType => isGroundConstr(tp.tp1) && isGroundConstr(tp.tp2) case _ => true instArg( if tvar.hasLowerBound then - if isGroundConstr(tvar.lowerBound) then tvar.instantiate(fromBelow = true) + if isGroundConstr(fullLowerBound(tvar.origin)) then tvar.instantiate(fromBelow = true) else if isFullyDefined(tp, ForceDegree.all) then tp else NoType else if tvar.hasUpperBound then - if isGroundConstr(tvar.upperBound) then tvar.instantiate(fromBelow = false) + if isGroundConstr(fullUpperBound(tvar.origin)) then tvar.instantiate(fromBelow = false) else if isFullyDefined(tp, ForceDegree.all) then tp else NoType - else NoType) + else + NoType) case _ => tp @@ -593,9 +596,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): resType <:< target val tparams = poly.paramRefs val variances = childClass.typeParams.map(_.paramVarianceSign) - val instanceTypes = tparams.lazyZip(variances).map((tparam, variance) => + val instanceTypes = tparams.lazyZip(variances).map: (tparam, variance) => TypeComparer.instanceType(tparam, fromBelow = variance < 0, Widen.Unions) - ) val instanceType = resType.substParams(poly, instanceTypes) // this is broken in tests/run/i13332intersection.scala, // because type parameters are not correctly inferred. diff --git a/tests/pos/i23611a.scala b/tests/pos/i23611a.scala new file mode 100644 index 000000000000..fbaf709e2f0e --- /dev/null +++ b/tests/pos/i23611a.scala @@ -0,0 +1,30 @@ +import java.io.{File, IOException} +import java.net.URI +import java.nio.file.{Path, Paths} +import scala.reflect.ClassTag + +trait FileConnectors { + def listPath(path: => Path): ZStream[Any, IOException, Path] + + final def listFile(file: => File): ZStream[Any, IOException, File] = + for { + path <- null.asInstanceOf[ZStream[Any, IOException, Path]] + r <- listPath(path).mapZIO(a => ZIO.attempt(a.toFile).refineToOrDie) + } yield r +} + +sealed abstract class CanFail[-E] +object CanFail: + given [E]: CanFail[E] = ??? + +sealed trait ZIO[-R, +E, +A] +extension [R, E <: Throwable, A](self: ZIO[R, E, A]) + def refineToOrDie[E1 <: E: ClassTag](using CanFail[E]): ZIO[R, E1, A] = ??? + +object ZIO: + def attempt[A](code: => A): ZIO[Any, Throwable, A] = ??? + +sealed trait ZStream[-R, +E, +A]: + def map[B](f: A => B): ZStream[R, E, B] = ??? + def flatMap[R1 <: R, E1 >: E, B](f: A => ZStream[R1, E1, B]): ZStream[R1, E1, B] + def mapZIO[R1 <: R, E1 >: E, A1](f: A => ZIO[R1, E1, A1]): ZStream[R1, E1, A1] \ No newline at end of file