From 6e675e09cdf46361bca0ab3468e3236f224b525c Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 6 Aug 2025 15:28:55 +0200 Subject: [PATCH] More careful ClassTag instantiation (#23659) We now use a blend of the new scheme and a backwards compatible special case if type variables as ClassTag arguments are constrained by further type variables. Fixes #23611 [Cherry-picked cfaa5d390dd225e54ea88383d7dd6e07f6f856de] --- .../dotty/tools/dotc/typer/Synthesizer.scala | 32 ++++++++++++++++--- tests/pos/i23611.scala | 26 +++++++++++++++ tests/pos/i23611a.scala | 30 +++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 tests/pos/i23611.scala create mode 100644 tests/pos/i23611a.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 8df2fc1ed4b7..0c35d0377e51 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): @@ -38,10 +39,32 @@ 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 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 + 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 tvar.instantiate(fromBelow = true) - else if tvar.hasUpperBound then tvar.instantiate(fromBelow = false) - else NoType) + if tvar.hasLowerBound then + 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(fullUpperBound(tvar.origin)) then tvar.instantiate(fromBelow = false) + else if isFullyDefined(tp, ForceDegree.all) then tp + else NoType + else + NoType) case _ => tp @@ -569,9 +592,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/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 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