From a1c1f74c3ef9e95b02d7e718d755231d084f3ac7 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Thu, 18 Dec 2025 15:18:48 +0100 Subject: [PATCH 1/2] Ignore selection prototypes when typing type applications Ignore selection prototypes at first when typing type applications. If we need them later for overloading disambiguation, reveal the ignored type. The reason for doing this is that a selection might come from an extension method, and in this case we should not require the selected name as a member of the result. This change breaks one test (overloading-specifity-2.scala) that explicitly tested that we don't consult implicit arguments for disambiguation since the expected type was a selection that already determined the outcome. This is logic no longer holds. We have to see whether this change breaks any code in practice. Fixes #23773 [Cherry-picked 30aa80596aa8f0b75fd9daa8ff15511902ca18bc][modified] --- compiler/src/dotty/tools/dotc/core/Types.scala | 5 +++++ .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 10 +++++++--- compiler/src/dotty/tools/dotc/typer/Applications.scala | 2 +- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 9 +++++++++ tests/{run => neg}/overloading-specifity-2.scala | 2 +- tests/pos/i23773.scala | 10 ++++++++++ 6 files changed, 33 insertions(+), 5 deletions(-) rename tests/{run => neg}/overloading-specifity-2.scala (94%) create mode 100644 tests/pos/i23773.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 41f7c89462c4..6b2453d66e7c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1859,6 +1859,11 @@ object Types extends TypeUtils { /** If this is a proto type, WildcardType, otherwise the type itself */ def dropIfProto: Type = this + /** If this is a (possibly applied) selection proto type, ignore the + * selection part + */ + def ignoreSelectionProto(using Context): Type = this + /** If this is an AndType, the number of factors, 1 for all other types */ def andFactorCount: Int = 1 diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 26936361cb2f..0af6e703d32b 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -306,9 +306,13 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ toText(resultType) ~ "]" case IgnoredProto(ignored) => - "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) - case tp @ PolyProto(targs, resType) => - "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) + "?" ~ ("(ignored: " ~ toText(ignored) ~ ")").provided(printDebug) + case tp @ PolyProto(targs, resultType) => + "[applied to [" + ~ toTextGlobal(targs, ", ") + ~ "] returning " + ~ toText(resultType) + ~ "]" case _ => super.toText(tp) } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 90cc7c9a3d84..3389edcd0417 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1248,7 +1248,7 @@ trait Applications extends Compatibility { val isNamed = hasNamedArg(tree.args) val typedArgs = if (isNamed) typedNamedArgs(tree.args) else tree.args.mapconserve(typedType(_)) record("typedTypeApply") - typedExpr(tree.fun, PolyProto(typedArgs, pt)) match { + typedExpr(tree.fun, PolyProto(typedArgs, pt.ignoreSelectionProto)) match { case fun: TypeApply if !ctx.isAfterTyper => val function = fun.fun val args = (fun.args ++ tree.args).map(_.show).mkString(", ") diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index f8bbd6989a0f..cc39612163a3 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -252,6 +252,9 @@ object ProtoTypes { override def deepenProtoTrans(using Context): SelectionProto = derivedSelectionProto(name, memberProto.deepenProtoTrans, compat, nameSpan) + override def ignoreSelectionProto(using Context): IgnoredProto = + IgnoredProto(this) + override def computeHash(bs: Hashable.Binders): Int = { val delta = (if (compat eq NoViewsAllowed) 1 else 0) | (if (privateOK) 2 else 0) addDelta(doHash(bs, name, memberProto), delta) @@ -584,6 +587,9 @@ object ProtoTypes { override def deepenProtoTrans(using Context): FunProto = derivedFunProto(args, resultType.deepenProtoTrans, constrainResultDeep = true) + override def ignoreSelectionProto(using Context): FunProto = + derivedFunProto(args, resultType.ignoreSelectionProto) + override def withContext(newCtx: Context): ProtoType = if newCtx `eq` protoCtx then this else new FunProto(args, resType)(typer, applyKind, state)(using newCtx) @@ -694,6 +700,9 @@ object ProtoTypes { override def deepenProtoTrans(using Context): PolyProto = derivedPolyProto(targs, resultType.deepenProtoTrans) + + override def ignoreSelectionProto(using Context): PolyProto = + derivedPolyProto(targs, resultType.ignoreSelectionProto) } /** A prototype for expressions [] that are known to be functions: diff --git a/tests/run/overloading-specifity-2.scala b/tests/neg/overloading-specifity-2.scala similarity index 94% rename from tests/run/overloading-specifity-2.scala rename to tests/neg/overloading-specifity-2.scala index 9b04d82bfa66..c6ae9799c06e 100644 --- a/tests/run/overloading-specifity-2.scala +++ b/tests/neg/overloading-specifity-2.scala @@ -23,5 +23,5 @@ object Test extends App { def foo[T]: Show[T] = new Show[T](2) } - assert(a.foo[Int].i == 1) // error: no implicit argument of type Test.Context was found for parameter ctx + assert(a.foo[Int].i == 1) // error: no implicit argument of type Test.Context was found for parameter ctx, was OK } \ No newline at end of file diff --git a/tests/pos/i23773.scala b/tests/pos/i23773.scala new file mode 100644 index 000000000000..7f3ce30ecc13 --- /dev/null +++ b/tests/pos/i23773.scala @@ -0,0 +1,10 @@ +trait Foo[T] + +def foo[A]: Int = ??? +def foo[A: Foo]: Int = ??? + +extension (x: Int) + def succ: Int = x + 1 + +val a = foo[Int] +val b = foo[Int].succ // error \ No newline at end of file From bf5865eb65bb91478bca047d79182877026df21c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Nov 2025 15:18:37 +0100 Subject: [PATCH 2/2] Refine isMatched of IgnoredProtos Fixes the Open CB counter example. [Cherry-picked 0436529fff50bdff9b0bdfade2180a04e963e28a] --- .../dotty/tools/dotc/typer/ProtoTypes.scala | 25 ++++++++++++++++--- tests/pos/i23773a.scala | 19 ++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 tests/pos/i23773a.scala diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index cc39612163a3..c41439c3b6a3 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -126,13 +126,15 @@ object ProtoTypes { override def viewExists(tp: Type, pt: Type)(using Context): Boolean = false } - /** A trait for prototypes that match all types */ - trait MatchAlways extends ProtoType { - def isMatchedBy(tp1: Type, keepConstraint: Boolean)(using Context): Boolean = true + /** A trait for prototypes that map to themselves */ + trait FixedProto extends ProtoType: def map(tm: TypeMap)(using Context): ProtoType = this def fold[T](x: T, ta: TypeAccumulator[T])(using Context): T = x override def toString: String = getClass.toString - } + + /** A trait for prototypes that match all types */ + trait MatchAlways extends FixedProto: + def isMatchedBy(tp1: Type, keepConstraint: Boolean)(using Context): Boolean = true /** A class marking ignored prototypes that can be revealed by `deepenProto` */ abstract case class IgnoredProto(ignored: Type) extends CachedGroundType with MatchAlways: @@ -143,6 +145,21 @@ object ProtoTypes { ignored override def deepenProtoTrans(using Context): Type = ignored.deepenProtoTrans + override def isMatchedBy(tp1: Type, keepConstraint: Boolean)(using Context): Boolean = + def takesParams(tp: Type): Boolean = tp match + case tp: PolyType => takesParams(tp.resType) + case MethodType(pnames) => pnames.nonEmpty && !tp.isImplicitMethod + case _ => false + ignored match + case ignored: SelectionProto if ignored.name != nme.apply => + // Non-implicit methods that take at least one parameter don't match ignored + // selection protos unless the selection is via `apply`. This is because a + // match of a different selection would require an eta expansion _and_ an + // implicit conversion, which is not allowed. So the prototype would not + // match even if implicit conversions were present. Test case: i23773a.scala. + !takesParams(tp1.widen) + case _ => true + /** Did someone look inside via deepenProto? Used for error deagniostics * to give a more extensive expected type. */ diff --git a/tests/pos/i23773a.scala b/tests/pos/i23773a.scala new file mode 100644 index 000000000000..eaf0d98b2956 --- /dev/null +++ b/tests/pos/i23773a.scala @@ -0,0 +1,19 @@ +trait NumericDate +trait JWSDecoded[H] + +trait StandardHeaderWrite[H]: + def setAlgorithm(header: H, algorithm: Algorithm): H + +object StandardHeaderWrite: + def apply[H](using sh: StandardHeaderWrite[H]): StandardHeaderWrite[H] = ??? + // unused - required to reproduce + def apply[H](setAlg: (H, Algorithm) => H): StandardHeaderWrite[H] = ??? + +final case class JWK(algorithm: Option[Algorithm]) +sealed trait Algorithm + +def Test[F[_], H](key: JWK, header: H)(using StandardHeaderWrite[H]) = { + key.algorithm + .map(StandardHeaderWrite[H].setAlgorithm(header, _)) + .getOrElse(header) +}