diff --git a/compiler/src/dotty/tools/dotc/core/Decorators.scala b/compiler/src/dotty/tools/dotc/core/Decorators.scala index ad00756769d9..809b76ba3118 100644 --- a/compiler/src/dotty/tools/dotc/core/Decorators.scala +++ b/compiler/src/dotty/tools/dotc/core/Decorators.scala @@ -56,6 +56,10 @@ object Decorators { termName(chars, 0, len) case name: TypeName => s.concat(name.toTermName) case _ => termName(s.concat(name.toString)) + + def indented(width: Int): String = + val padding = " " * width + padding + s.replace("\n", "\n" + padding) end extension /** Implements a findSymbol method on iterators of Symbols that diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 32900e9e66c9..d0b7e811eb4b 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -137,21 +137,6 @@ object NameOps { else name.toTermName } - /** Does the name match `extension`? */ - def isExtension: Boolean = name match - case name: SimpleName => - name.length == "extension".length && name.startsWith("extension") - case _ => false - - /** Does this name start with `extension_`? */ - def isExtensionName: Boolean = name match - case name: SimpleName => name.startsWith("extension_") - case _ => false - - // TODO: Drop next 3 methods once extension names have stabilized - /** Add an `extension_` in front of this name */ - def toExtensionName(using Context): SimpleName = "extension_".concat(name) - /** The expanded name. * This is the fully qualified name of `base` with `ExpandPrefixName` as separator, * followed by `kind` and the name. diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 4541c3942857..ba0337a797f3 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -383,8 +383,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase def checkIdent(sel: untpd.ImportSelector): Unit = if !exprTpe.member(sel.name).exists - && !exprTpe.member(sel.name.toTypeName).exists - && !exprTpe.member(sel.name.toExtensionName).exists then + && !exprTpe.member(sel.name.toTypeName).exists then report.error(NotAMember(exprTpe, sel.name, "value"), sel.imported.srcPos) if seen.contains(sel.name) then report.error(ImportRenamedTwice(sel.imported), sel.imported.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 10f770f6ce42..85a3be7b6fdd 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2153,19 +2153,11 @@ trait Applications extends Compatibility { (tree, currentPt) val (core, pt1) = normalizePt(methodRef, pt) - val app = withMode(Mode.SynthesizeExtMethodReceiver) { + withMode(Mode.SynthesizeExtMethodReceiver) { typed( untpd.Apply(core, untpd.TypedSplice(receiver, isExtensionReceiver = true) :: Nil), pt1, ctx.typerState.ownedVars) } - def isExtension(tree: Tree): Boolean = methPart(tree) match { - case Inlined(call, _, _) => isExtension(call) - case tree @ Select(qual, nme.apply) => tree.symbol.is(ExtensionMethod) || isExtension(qual) - case tree => tree.symbol.is(ExtensionMethod) - } - if (!isExtension(app)) - report.error(em"not an extension method: $methodRef", receiver.srcPos) - app } def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) = diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 9db5d7dbf697..78fca17c4758 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -13,6 +13,7 @@ import util.SrcPos import config.Feature import java.util.regex.Matcher.quoteReplacement import reporting._ +import collection.mutable import scala.util.matching.Regex @@ -140,26 +141,47 @@ object ErrorReporting { if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite." else "" + def whyFailedStr(fail: FailedExtension) = + i""" failed with + | + |${fail.whyFailed.message.indented(8)}""" + def selectErrorAddendum (tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String) (using Context): String = - val attempts: List[Tree] = qual1.getAttachment(Typer.HiddenSearchFailure) match - case Some(failures) => - for failure <- failures - if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits] - yield failure.tree - case _ => Nil + + val attempts = mutable.ListBuffer[(Tree, String)]() + val nested = mutable.ListBuffer[NestedFailure]() + for + failures <- qual1.getAttachment(Typer.HiddenSearchFailure) + failure <- failures + do + failure.reason match + case fail: NestedFailure => nested += fail + case fail: FailedExtension => attempts += ((failure.tree, whyFailedStr(fail))) + case fail: Implicits.NoMatchingImplicits => // do nothing + case _ => attempts += ((failure.tree, "")) if qualType.derivesFrom(defn.DynamicClass) then "\npossible cause: maybe a wrong Dynamic method signature?" else if attempts.nonEmpty then - val attemptStrings = attempts.map(_.showIndented(4)).distinct + val attemptStrings = + attempts.toList + .map((tree, whyFailed) => (tree.showIndented(4), whyFailed)) + .distinctBy(_._1) + .map((treeStr, whyFailed) => + i""" + | $treeStr$whyFailed""") val extMethods = if attemptStrings.length > 1 then "Extension methods were" else "An extension method was" i""". |$extMethods tried, but could not be fully constructed: + |$attemptStrings%\n%""" + else if nested.nonEmpty then + i""". + |Extension methods were tried, but the search failed with: | - | $attemptStrings%\nor\n %""" + | ${nested.head.explanation}""" else if tree.hasAttachment(desugar.MultiLineInfix) then i""". |Note that `${tree.name}` is treated as an infix operator in Scala 3. diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index bdf1f03f2e2e..d532f070ff3a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -76,9 +76,9 @@ object Implicits: */ def hasExtMethod(tp: Type, expected: Type)(using Context) = expected match case selProto @ SelectionProto(selName: TermName, _, _, _) => - tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists - || tp.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists - case _ => false + tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists + case _ => + false def strictEquality(using Context): Boolean = ctx.mode.is(Mode.StrictEquality) || Feature.enabled(nme.strictEquality) @@ -513,9 +513,19 @@ object Implicits: em"${err.refStr(ref)} produces a diverging implicit search when trying to $qualify" } - class FailedExtension(extApp: Tree, val expectedType: Type) extends SearchFailureType: + /** A search failure type for attempted ill-typed extension method calls */ + class FailedExtension(extApp: Tree, val expectedType: Type, val whyFailed: Message) extends SearchFailureType: def argument = EmptyTree def explanation(using Context) = em"$extApp does not $qualify" + + /** A search failure type for aborted searches of extension methods, typically + * because of a cyclic reference or similar. + */ + class NestedFailure(_msg: Message, val expectedType: Type) extends SearchFailureType: + def argument = EmptyTree + override def msg(using Context) = _msg + def explanation(using Context) = msg.toString + end Implicits import Implicits._ @@ -1013,12 +1023,7 @@ trait Implicits: pt match case selProto @ SelectionProto(selName: TermName, mbrType, _, _) if cand.isExtension => def tryExtension(using Context) = - val xname = - if ref.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists then - selProto.extensionName - else - selName - extMethodApply(untpd.Select(untpdGenerated, xname), argument, mbrType) + extMethodApply(untpd.Select(untpdGenerated, selName), argument, mbrType) if cand.isConversion then val extensionCtx, conversionCtx = ctx.fresh.setNewTyperState() val extensionResult = tryExtension(using extensionCtx) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 4b04666a2cb8..1ac41be3b450 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -147,11 +147,6 @@ object ProtoTypes { abstract case class SelectionProto(name: Name, memberProto: Type, compat: Compatibility, privateOK: Boolean) extends CachedProxyType with ProtoType with ValueTypeOrProto { - private var myExtensionName: TermName = null - def extensionName(using Context): TermName = - if myExtensionName == null then myExtensionName = name.toExtensionName - myExtensionName - /** Is the set of members of this type unknown? This is the case if: * 1. The type has Nothing or Wildcard as a prefix or underlying type * 2. The type has an uninstantiated TypeVar as a prefix or underlying type, @@ -447,8 +442,7 @@ object ProtoTypes { ctx.typer.isApplicableType(tp, argType :: Nil, resultType) || { resType match { case selProto @ SelectionProto(selName: TermName, mbrType, _, _) => - ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType) - || ctx.typer.hasExtensionMethodNamed(tp, selProto.extensionName, argType, mbrType) + ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType) //.reporting(i"has ext $tp $name $argType $mbrType: $result") case _ => false diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 8b0034510bc6..ad4eb742fd47 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3348,11 +3348,8 @@ class Typer extends Namer // implicit conversion to the receiver type. def sharpenedPt = pt match case pt: SelectionProto - if pt.name.isExtensionName - || pt.memberProto.revealIgnored.isExtensionApplyProto => - pt.deepenProto - case _ => - pt + if pt.memberProto.revealIgnored.isExtensionApplyProto => pt.deepenProto + case _ => pt def adaptNoArgs(wtp: Type): Tree = { val ptNorm = underlyingApplied(pt) @@ -3499,24 +3496,27 @@ class Typer extends Namer // try an extension method in scope pt match { case selProto @ SelectionProto(selName: TermName, mbrType, _, _) => + def tryExtension(using Context): Tree = - try - findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match - case ref: TermRef => - extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType) - case _ => findRef(selProto.extensionName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match - case ref: TermRef => - extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType) - case _ => EmptyTree - catch case ex: TypeError => errorTree(tree, ex, tree.srcPos) - val nestedCtx = ctx.fresh.setNewTyperState() - val app = tryExtension(using nestedCtx) - if (!app.isEmpty && !nestedCtx.reporter.hasErrors) { - nestedCtx.typerState.commit() - return ExtMethodApply(app) - } - else if !app.isEmpty then - rememberSearchFailure(tree, SearchFailure(app.withType(FailedExtension(app, pt)))) + findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match + case ref: TermRef => + extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType) + case _ => + EmptyTree + + try + val nestedCtx = ctx.fresh.setNewTyperState() + val app = tryExtension(using nestedCtx) + if !app.isEmpty && !nestedCtx.reporter.hasErrors then + nestedCtx.typerState.commit() + return ExtMethodApply(app) + else + for err <- nestedCtx.reporter.allErrors.take(1) do + rememberSearchFailure(tree, + SearchFailure(app.withType(FailedExtension(app, pt, err.msg)))) + catch case ex: TypeError => + rememberSearchFailure(tree, + SearchFailure(tree.withType(NestedFailure(ex.toMessage, pt)))) case _ => } diff --git a/tests/neg/enum-values.check b/tests/neg/enum-values.check index 3d1f3e84b805..3b22071aa808 100644 --- a/tests/neg/enum-values.check +++ b/tests/neg/enum-values.check @@ -6,7 +6,10 @@ | meaning a values array is not defined. | An extension method was tried, but could not be fully constructed: | - | example.Extensions.values(Tag) + | example.Extensions.values(Tag) failed with + | + | Found: example.Tag.type + | Required: Nothing -- [E008] Not Found Error: tests/neg/enum-values.scala:33:50 ----------------------------------------------------------- 33 | val listlikes: Array[ListLike[?]] = ListLike.values // error | ^^^^^^^^^^^^^^^ @@ -15,7 +18,10 @@ | meaning a values array is not defined. | An extension method was tried, but could not be fully constructed: | - | example.Extensions.values(ListLike) + | example.Extensions.values(ListLike) failed with + | + | Found: example.ListLike.type + | Required: Nothing -- [E008] Not Found Error: tests/neg/enum-values.scala:34:52 ----------------------------------------------------------- 34 | val typeCtorsK: Array[TypeCtorsK[?]] = TypeCtorsK.values // error | ^^^^^^^^^^^^^^^^^ @@ -24,7 +30,10 @@ | meaning a values array is not defined. | An extension method was tried, but could not be fully constructed: | - | example.Extensions.values(TypeCtorsK) + | example.Extensions.values(TypeCtorsK) failed with + | + | Found: example.TypeCtorsK.type + | Required: Nothing -- [E008] Not Found Error: tests/neg/enum-values.scala:36:6 ------------------------------------------------------------ 36 | Tag.valueOf("Int") // error | ^^^^^^^^^^^ @@ -54,7 +63,10 @@ | value values is not a member of object example.NotAnEnum. | An extension method was tried, but could not be fully constructed: | - | example.Extensions.values(NotAnEnum) + | example.Extensions.values(NotAnEnum) failed with + | + | Found: example.NotAnEnum.type + | Required: Nothing -- [E008] Not Found Error: tests/neg/enum-values.scala:41:12 ----------------------------------------------------------- 41 | NotAnEnum.valueOf("Foo") // error | ^^^^^^^^^^^^^^^^^ diff --git a/tests/neg/i10870.check b/tests/neg/i10870.check new file mode 100644 index 000000000000..857eb128b19f --- /dev/null +++ b/tests/neg/i10870.check @@ -0,0 +1,7 @@ +-- [E008] Not Found Error: tests/neg/i10870.scala:10:16 ---------------------------------------------------------------- +10 | def x = b.a.x // error + | ^^^^^ + | value x is not a member of A. + | Extension methods were tried, but the search failed with: + | + | Overloaded or recursive method x needs return type diff --git a/tests/neg/i10870.scala b/tests/neg/i10870.scala new file mode 100644 index 000000000000..55669d580dc5 --- /dev/null +++ b/tests/neg/i10870.scala @@ -0,0 +1,10 @@ +final case class A() +final case class B(a:A) + +object Test: + + extension(a:A) + def x = 5 + + extension(b:B) + def x = b.a.x // error diff --git a/tests/neg/i10901.check b/tests/neg/i10901.check new file mode 100644 index 000000000000..b11f7952bea7 --- /dev/null +++ b/tests/neg/i10901.check @@ -0,0 +1,41 @@ +-- [E008] Not Found Error: tests/neg/i10901.scala:45:38 ---------------------------------------------------------------- +45 | val pos1: Point2D[Int,Double] = x º y // error + | ^^^ + | value º is not a member of object BugExp4Point2D.IntT. + | An extension method was tried, but could not be fully constructed: + | + | º(x) failed with + | + | Ambiguous overload. The overloaded alternatives of method º in object dsl with types + | [T1, T2] + | (x: BugExp4Point2D.ColumnType[T1]) + | (y: BugExp4Point2D.ColumnType[T2]) + | (implicit evidence$7: Numeric[T1], evidence$8: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | [T1, T2] + | (x: T1) + | (y: BugExp4Point2D.ColumnType[T2]) + | (implicit evidence$5: Numeric[T1], evidence$6: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | both match arguments ((x : BugExp4Point2D.IntT.type)) +-- [E008] Not Found Error: tests/neg/i10901.scala:48:38 ---------------------------------------------------------------- +48 | val pos4: Point2D[Int,Double] = x º 201.1 // error + | ^^^ + |value º is not a member of object BugExp4Point2D.IntT. + |An extension method was tried, but could not be fully constructed: + | + | º(x) failed with + | + | Ambiguous overload. The overloaded alternatives of method º in object dsl with types + | [T1, T2] + | (x: BugExp4Point2D.ColumnType[T1]) + | (y: T2)(implicit evidence$9: Numeric[T1], evidence$10: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | [T1, T2](x: T1)(y: T2)(implicit evidence$3: Numeric[T1], evidence$4: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | both match arguments ((x : BugExp4Point2D.IntT.type)) +-- [E008] Not Found Error: tests/neg/i10901.scala:62:16 ---------------------------------------------------------------- +62 | val y = "abc".foo // error + | ^^^^^^^^^ + | value foo is not a member of String. + | An extension method was tried, but could not be fully constructed: + | + | Test.foo("abc")(/* missing */summon[C]) failed with + | + | no implicit argument of type C was found for parameter x$1 of method foo in object Test diff --git a/tests/neg/i10901.scala b/tests/neg/i10901.scala new file mode 100644 index 000000000000..9552047c402f --- /dev/null +++ b/tests/neg/i10901.scala @@ -0,0 +1,63 @@ +import scala.annotation.targetName + +object BugExp4Point2D { + + sealed trait ColumnType[T] + case object DoubleT extends ColumnType[Double] + case object IntT extends ColumnType[Int] + + object dsl { + + + extension [T1:Numeric, T2:Numeric](x: T1) + + // N - N + @targetName("point2DConstant") + def º(y: T2): Point2D[T1,T2] = ??? + + + // N - C + @targetName("point2DConstantData") + def º(y: ColumnType[T2]): Point2D[T1,T2] = ??? + + + + extension [T1:Numeric, T2:Numeric](x: ColumnType[T1]) + // C - C + @targetName("point2DData") + def º(y: ColumnType[T2]): Point2D[T1,T2] = ??? + + // C - N + @targetName("point2DDataConstant") + def º(y: T2): Point2D[T1,T2] = ??? + + + } + + case class Point2D[T1:Numeric, T2:Numeric](x:T1, y:T2) + + import dsl._ + + def main(args: Array[String]): Unit = { + val x = IntT + val y = DoubleT + + val pos1: Point2D[Int,Double] = x º y // error + val pos2: Point2D[Int,Double] = 100 º 200.1 // ok + val pos3: Point2D[Int,Double] = 101 º y // ok + val pos4: Point2D[Int,Double] = x º 201.1 // error + + } +} + +class C + +object Container: + given C with {} + +object Test: + extension (x: String)(using C) + def foo: String = x + + val y = "abc".foo // error + diff --git a/tests/neg/i6183.check b/tests/neg/i6183.check index 0f09c0b0c166..144258bd080a 100644 --- a/tests/neg/i6183.check +++ b/tests/neg/i6183.check @@ -4,7 +4,12 @@ | value render is not a member of Int. | An extension method was tried, but could not be fully constructed: | - | render(42) + | render(42) failed with + | + | Ambiguous overload. The overloaded alternatives of method render in object Test with types + | [B](b: B)(using x$1: DummyImplicit): Char + | [A](a: A): String + | both match arguments ((42 : Int)) -- [E051] Reference Error: tests/neg/i6183.scala:7:9 ------------------------------------------------------------------- 7 | Test.render(42) // error | ^^^^^^^^^^^ diff --git a/tests/neg/i7056.check b/tests/neg/i7056.check new file mode 100644 index 000000000000..2dff4dc66b13 --- /dev/null +++ b/tests/neg/i7056.check @@ -0,0 +1,7 @@ +-- [E008] Not Found Error: tests/neg/i7056.scala:19:10 ----------------------------------------------------------------- +19 |val z = x.idnt1 // error + | ^^^^^^^ + | value idnt1 is not a member of B. + | An extension method was tried, but could not be fully constructed: + | + | i7056$package.given_T1_T[T](given_PartialId_B).idnt1() diff --git a/tests/neg/i7056.scala b/tests/neg/i7056.scala new file mode 100644 index 000000000000..d16aa949000e --- /dev/null +++ b/tests/neg/i7056.scala @@ -0,0 +1,19 @@ +type A +type B <: A + +type PartialId[X] = X match { + case B => X +} + +trait T1[T] { + extension (t1: T) def idnt1: Any +} + +given [T <: A](using PartialId[T]): T1[T] = new T1[T] { + extension (t1: T) def idnt1: Any = ??? +} + +given PartialId[B] = ??? + +val x: B = ??? +val z = x.idnt1 // error diff --git a/tests/neg/i9185.check b/tests/neg/i9185.check index c469b447742e..1ba971fccff7 100644 --- a/tests/neg/i9185.check +++ b/tests/neg/i9185.check @@ -6,7 +6,9 @@ | | M.pure[A, F]("ola")( | /* ambiguous: both object listMonad in object M and object optionMonad in object M match type M[F] */summon[M[F]] - | ) + | ) failed with + | + | ambiguous implicit arguments: both object listMonad in object M and object optionMonad in object M match type M[F] of parameter m of method pure in object M -- Error: tests/neg/i9185.scala:8:28 ----------------------------------------------------------------------------------- 8 | val value3 = M.pure("ola") // error | ^ @@ -17,4 +19,7 @@ | value len is not a member of String. | An extension method was tried, but could not be fully constructed: | - | M.len("abc") + | M.len("abc") failed with + | + | Found: ("abc" : String) + | Required: Int diff --git a/tests/run/eta-extension.scala b/tests/run/eta-extension.scala new file mode 100644 index 000000000000..edf83601c0b5 --- /dev/null +++ b/tests/run/eta-extension.scala @@ -0,0 +1,6 @@ +extension (s: String) def times(i: Int): String = s * i + +val partial = "abc".times + +@main def Test = + assert(partial(2) == "abcabc") diff --git a/tests/run/i9507.scala b/tests/run/i9507.scala new file mode 100644 index 000000000000..5d8923a03021 --- /dev/null +++ b/tests/run/i9507.scala @@ -0,0 +1,6 @@ +extension (x: Int) + def add(y: Int): Int = x + y + +def newFunction: Int => Int = 4.add + +@main def Test = assert(newFunction(1) == 5) \ No newline at end of file diff --git a/tests/run/sticky-extmethod.check b/tests/run/sticky-extmethod.check new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/tests/run/sticky-extmethod.check @@ -0,0 +1 @@ +1 diff --git a/tests/run/sticky-extmethod.scala b/tests/run/sticky-extmethod.scala new file mode 100644 index 000000000000..40ffad61dc35 --- /dev/null +++ b/tests/run/sticky-extmethod.scala @@ -0,0 +1,5 @@ + +extension (x: Int) def m (y: Object): Int = x +def m (x: Int)(y: String): String = y * x + +@main def Test = println(1.m("xx"))