From b677f97a1fa26e0c27a4f434e850b4c8214d606c Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Aug 2025 19:44:59 +0200 Subject: [PATCH 1/3] 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 --- compiler/src/dotty/tools/dotc/core/Types.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index a89a4cb95808..8041be85d61e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5117,11 +5117,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 From e4b8f3c7ad6cb727dafee0509db4fb7a44e3a9c2 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 6 Aug 2025 11:39:38 +0200 Subject: [PATCH 2/3] Refine criterion when to use fullyDefinedType in ClassTag search --- compiler/src/dotty/tools/dotc/core/Types.scala | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8041be85d61e..a89a4cb95808 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5117,17 +5117,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 From 975c988ed23818c033a433fd91e5c12ec909f359 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Aug 2025 12:04:55 +0200 Subject: [PATCH 3/3] Use more context for implicit search only if no default argument After an "implicit not found", we type additional arguments to get more context which might give a larger implicit scope to search. With this commit we do that only if there is no default argument for the implicit. This might fix #23610 --- .../src/dotty/tools/dotc/typer/Typer.scala | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c9d8fc0c1e0d..413f55b37587 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4362,11 +4362,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val arg = inferImplicitArg(formal, tree.span.endPos) + lazy val defaultArg = findDefaultArgument(argIndex) + .showing(i"default argument: for $formal, $tree, $argIndex = $result", typr) + def argHasDefault = hasDefaultParams && !defaultArg.isEmpty + def canProfitFromMoreConstraints = arg.tpe.isInstanceOf[AmbiguousImplicits] - // ambiguity could be decided by more constraints - || !isFullyDefined(formal, ForceDegree.none) - // more context might constrain type variables which could make implicit scope larger + // Ambiguity could be decided by more constraints + || !isFullyDefined(formal, ForceDegree.none) && !argHasDefault + // More context might constrain type variables which could make implicit scope larger. + // But in this case we should search with additional arguments typed only if there + // is no default argument. // Try to constrain the result using `pt1`, but back out if a BadTyperStateAssertion // is thrown. TODO Find out why the bad typer state arises and prevent it. The try-catch @@ -4388,15 +4394,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case failed: AmbiguousImplicits => arg :: implicitArgs(formals1, argIndex + 1, pt) case failed: SearchFailureType => - lazy val defaultArg = findDefaultArgument(argIndex) - .showing(i"default argument: for $formal, $tree, $argIndex = $result", typr) - if !hasDefaultParams || defaultArg.isEmpty then - // no need to search further, the adapt fails in any case - // the reason why we continue inferring arguments in case of an AmbiguousImplicits - // is that we need to know whether there are further errors. - // If there are none, we have to propagate the ambiguity to the caller. - arg :: formals1.map(dummyArg) - else + if argHasDefault then // This is tricky. On the one hand, we need the defaultArg to // correctly type subsequent formal parameters in the same using // clause in case there are parameter dependencies. On the other hand, @@ -4407,6 +4405,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // `if propFail.exists` where we re-type the whole using clause with named // arguments for all implicits that were found. arg :: inferArgsAfter(defaultArg) + else + // no need to search further, the adapt fails in any case + // the reason why we continue inferring arguments in case of an AmbiguousImplicits + // is that we need to know whether there are further errors. + // If there are none, we have to propagate the ambiguity to the caller. + arg :: formals1.map(dummyArg) case _ => arg :: inferArgsAfter(arg) end implicitArgs