From 30aa80596aa8f0b75fd9daa8ff15511902ca18bc Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Nov 2025 13:14:56 +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 --- compiler/src/dotty/tools/dotc/core/Types.scala | 5 +++++ .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 8 ++++++-- compiler/src/dotty/tools/dotc/typer/Applications.scala | 3 +-- 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, 32 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 ca9eda247cc0..5d630cad6368 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1956,6 +1956,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 9f4cc2086dc1..37222fba3ee3 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -354,8 +354,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ "]" case IgnoredProto(ignored) => "?" ~ ("(ignored: " ~ toText(ignored) ~ ")").provided(printDebug) - case tp @ PolyProto(targs, resType) => - "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) + 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 5944a586d1cf..b8dbfc3786fe 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1422,7 +1422,7 @@ trait Applications extends Compatibility { 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(", ") @@ -1446,7 +1446,6 @@ trait Applications extends Compatibility { } if (typedFn.tpe eq TryDynamicCallType) tryDynamicTypeApply() else assignType(cpy.TypeApply(tree)(typedFn, typedArgs), typedFn, typedArgs) - } } /** Rewrite `new Array[T](....)` if T is an unbounded generic to calls to newGenericArray. diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 4fc092d16007..140d4beed4a2 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -286,6 +286,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) @@ -620,6 +623,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) @@ -734,6 +740,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 0436529fff50bdff9b0bdfade2180a04e963e28a 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. --- .../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 140d4beed4a2..878126b6331f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -162,13 +162,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: @@ -179,6 +181,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) +}