Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 74 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,96 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
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
// 2. Otherwise, if the bound is itself an unsolved type variable whose
// transitive bound (in the same direction) is ground, first
// instantiate that parent tvar from its bound, then re-check (1).
// This handles patterns like `refineToOrDie` inside `Managed.make`
// where the immediate upper bound is another type variable, but
// transitively reachable is `Throwable`.
// 3. 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
// 4. Otherwise leave argument uninstantiated.
// The reason for (3) 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
// So in order to keep backwards compatibility (where before we _only_ did 3) we
// add that special case.
// The reason for (2) is the related zio-jms pattern; see test for reference.
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
// Walk the chain of unsolved tvars (or unsolved type-param refs that
// map to tvars) in the bound direction, instantiating any whose own
// bound is ground. Returns true if any progress was made and the
// original bound is now ground.
def asTypeVar(tp: Type): TypeVar | Null = tp.dealias match
case tv: TypeVar if ctx.typerState.constraint.contains(tv) => tv
case pref: TypeParamRef if ctx.typerState.constraint.contains(pref) =>
ctx.typerState.constraint.typeVarOfParam(pref) match
case tv: TypeVar => tv
case _ => null
// An AndType whose components are =:= (e.g. `TypeVar(E) & TypeParamRef(E)`)
// is logically a single tvar; both components track the same underlying
// constraint variable.
case at: AndType if at.tp1 =:= at.tp2 => asTypeVar(at.tp1)
case _ => null
def forceGround(currentBound: Type, seen: Set[TypeVar], fromBelow: Boolean): Boolean =
val parent = asTypeVar(currentBound)
if parent == null || seen.contains(parent) then false
else
val parentBound =
if fromBelow then fullLowerBound(parent.origin)
else fullUpperBound(parent.origin)
if isGroundConstr(parentBound) then
parent.instantiate(fromBelow); true
else if forceGround(parentBound, seen + parent, fromBelow) then
val recheckedBound =
if fromBelow then fullLowerBound(parent.origin)
else fullUpperBound(parent.origin)
if isGroundConstr(recheckedBound) then
parent.instantiate(fromBelow); true
else false
else false
// After isFullyDefined succeeds with `ForceDegree.all`, `tp.dealias`
// may be a bottom type if the only available default for an unsolved
// tvar in the bound chain is its lower bound (e.g. `Nothing`).
// In that case, walk the chain of unsolved tvars in the bound
// direction and instantiate any whose own bound is ground.
// This handles the i26003 (zio-jms) pattern, where:
// - `E1 <: E` and `E <: Throwable` are the only constraints,
// - isFullyDefined defaults `E1` to `Nothing` (bottom),
// - we'd rather use `Throwable` so `ClassTag[Throwable]` is
// synthesized.
// The helper checks the *post-instantiation* state via the still-live
// chain of unconstrained tvars, so it is safe to call even after
// isFullyDefined has run.
instArg(
if tvar.hasLowerBound then
if isGroundConstr(fullLowerBound(tvar.origin)) then tvar.instantiate(fromBelow = true)
// Try the chain-walking step first when the bound is itself an
// unsolved tvar; this avoids `isFullyDefined` defaulting an
// unsolved tvar to its lower bound (e.g. `Nothing`).
else if forceGround(fullLowerBound(tvar.origin), Set(tvar), fromBelow = true)
&& ctx.typerState.constraint.contains(tvar)
&& 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)
// Prefer `failBottom`: if the only way to fully define `tp` is
// to default an unsolved tvar to its lower bound (a bottom
// type), bail and let the chain-walking fallback take over.
else if isFullyDefined(tp, ForceDegree.failBottom) then tp
else if ctx.typerState.constraint.contains(tvar)
&& forceGround(fullUpperBound(tvar.origin), Set(tvar), fromBelow = false)
&& isGroundConstr(fullUpperBound(tvar.origin))
then tvar.instantiate(fromBelow = false)
// Last resort: full force, accepting bottom defaults. Preserves
// backward compatibility for cases like i23611 where the
// surrounding flow already pinned the answer.
else if isFullyDefined(tp, ForceDegree.all) then tp
else NoType
else
Expand Down
26 changes: 26 additions & 0 deletions tests/pos/i26005.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Companion to i23611: ClassTag synthesis must instantiate a TypeVar whose
// upper bound is itself another unsolved TypeVar, when the transitive bound is
// ground. Surfaced by the dobrynya/zio-jms VirtusLab community-build run.
import scala.reflect.ClassTag

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 attemptBlocking[A](code: => A): ZIO[Any, Throwable, A] = ???

class Producer { def close(): Unit = () }
class Session:
def createProducer(dest: Object): Producer = new Producer

sealed trait Managed[-R, +E, +A]
object Managed:
def make[R, E, A](acquire: ZIO[R, E, A])(release: A => ZIO[R, Nothing, Unit]): Managed[R, E, A] = ???

object UIO:
def apply(code: => Unit): ZIO[Any, Nothing, Unit] = ???

object Repro:
def producer(session: Session) =
Managed.make(ZIO.attemptBlocking(session.createProducer(null)).refineToOrDie)(p => UIO(p.close()))
Loading