diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 0517d5b46b9e..acfbd7a1571c 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -465,6 +465,7 @@ private sealed trait YSettings: val Yinstrument: Setting[Boolean] = BooleanSetting(ForkSetting, "Yinstrument", "Add instrumentation code that counts allocations and closure creations.") val YinstrumentDefs: Setting[Boolean] = BooleanSetting(ForkSetting, "Yinstrument-defs", "Add instrumentation code that counts method calls; needs -Yinstrument to be set, too.") + val YimplicitToGiven: Setting[Boolean] = BooleanSetting(ForkSetting, "Yimplicit-to-given", "Allows to rewrite the implicit keywords to their scala-3 given counterparts. Does not adjust imports. Use in conjunction with --rewrite.") // Deprecated: lifted from -Y to -X @deprecated(message = "Lifted to -X, Scheduled for removal.", since = "3.5.0") diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index d6db7348ddc1..96276be45914 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -8,6 +8,8 @@ import ContextOps.enter import TypeApplications.EtaExpansion import collection.mutable import config.Printers.typr +import rewrites.Rewrites.patch +import util.Spans.Span /** Operations that are shared between Namer and TreeUnpickler */ object NamerOps: @@ -59,12 +61,19 @@ object NamerOps: * This is done by adding a () in front of a leading old style implicit parameter, * or by adding a () as last -- or only -- parameter list if the constructor has * only using clauses as parameters. + * + * implicitRewritePosition, if included, will point to where `()` should be added if rewriting + * with -Yimplicit-to-given */ - def normalizeIfConstructor(paramss: List[List[Symbol]], isConstructor: Boolean)(using Context): List[List[Symbol]] = + def normalizeIfConstructor(paramss: List[List[Symbol]], isConstructor: Boolean, implicitRewritePosition: Option[Span] = None)(using Context): List[List[Symbol]] = if !isConstructor then paramss else paramss match - case TypeSymbols(tparams) :: paramss1 => tparams :: normalizeIfConstructor(paramss1, isConstructor) - case TermSymbols(vparam :: _) :: _ if vparam.is(Implicit) => Nil :: paramss + case TypeSymbols(tparams) :: paramss1 => tparams :: normalizeIfConstructor(paramss1, isConstructor, implicitRewritePosition) + case TermSymbols(vparam :: _) :: _ if vparam.is(Implicit) => + implicitRewritePosition match + case Some(position) if ctx.settings.YimplicitToGiven.value => patch(position, "()") + case _ => () + Nil :: paramss case _ => if paramss.forall { case TermSymbols(vparams) => vparams.nonEmpty && vparams.head.is(Given) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index e396839c972e..c3d9657e7755 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3579,7 +3579,11 @@ object Parsers { def paramMods() = if in.token == IMPLICIT then - addParamMod(() => Mod.Implicit()) + addParamMod(() => + if ctx.settings.YimplicitToGiven.value then + patch(Span(in.lastOffset - 8, in.lastOffset), "using") + Mod.Implicit() + ) else if isIdent(nme.using) then if initialMods.is(Given) then syntaxError(em"`using` is already implied here, should not be given explicitly", in.offset) diff --git a/compiler/src/dotty/tools/dotc/typer/Migrations.scala b/compiler/src/dotty/tools/dotc/typer/Migrations.scala index acf9e7668917..5311199aad24 100644 --- a/compiler/src/dotty/tools/dotc/typer/Migrations.scala +++ b/compiler/src/dotty/tools/dotc/typer/Migrations.scala @@ -132,10 +132,7 @@ trait Migrations: if tp.companion == ImplicitMethodType && pt.applyKind != ApplyKind.Using && pt.args.nonEmpty then // The application can only be rewritten if it uses parentheses syntax. // See issue #22927 and related tests. - val hasParentheses = - ctx.source.content - .slice(tree.span.end, pt.args.head.span.start) - .exists(_ == '(') + val hasParentheses = checkParentheses(tree, pt) val rewriteMsg = if hasParentheses then Message.rewriteNotice("This code", mversion.patchFrom) @@ -148,7 +145,68 @@ trait Migrations: |""", pt.args.head.srcPos, mversion) if hasParentheses && mversion.needsPatch then - patch(Span(pt.args.head.span.start), "using ") + patchImplicitParams(tree, pt) end implicitParams + private def checkParentheses(tree: Tree, pt: FunProto)(using Context): Boolean = + ctx.source.content + .slice(tree.span.end, pt.args.head.span.start) + .exists(_ == '(') + + private def patchImplicitParams(tree: Tree, pt: FunProto)(using Context): Unit = + patch(Span(pt.args.head.span.start), "using ") + + object ImplicitToGiven: + def valDef(vdef: ValDef)(using Context): Unit = + if ctx.settings.YimplicitToGiven.value + && vdef.symbol.is(Implicit) + && !vdef.symbol.isParamOrAccessor + then + val implicitSpan = + vdef.mods.mods.collectFirst { + case mod: untpd.Mod.Implicit => mod.span + }.get + patch( + Span(implicitSpan.start, implicitSpan.end + 1), + "" + ) + patch( + Span(vdef.mods.mods.last.span.end + 1, vdef.namePos.span.start), "given " + ) + + def defDef(ddef: DefDef)(using Context): Unit = + if + ctx.settings.YimplicitToGiven.value + && ddef.symbol.is(Implicit) + && !ddef.symbol.isParamOrAccessor + && !ddef.symbol.isOldStyleImplicitConversion() + then + val implicitSpan = + ddef.mods.mods.collectFirst { + case mod: untpd.Mod.Implicit => mod.span + }.get + patch( + Span(implicitSpan.start, implicitSpan.end + 1), "" + ) + patch( + Span(ddef.mods.mods.last.span.end + 1, ddef.namePos.span.start), "given " + ) + ddef.tpt match + case refinedType: untpd.RefinedTypeTree => + patch(refinedType.span.startPos, "(") + patch(refinedType.span.endPos, ")") + case _ => + + def implicitParams(tree: Tree, tp: MethodOrPoly, pt: FunProto)(using Context): Unit = + if + ctx.settings.YimplicitToGiven.value + && !mv.ExplicitContextBoundArgument.needsPatch // let's not needlessly repeat the patch + && tp.companion == ImplicitMethodType + && pt.applyKind != ApplyKind.Using + && pt.args.nonEmpty + && checkParentheses(tree, pt) + then + patchImplicitParams(tree, pt) + + end Migrations diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 408018daf82c..76328aabb573 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1941,7 +1941,7 @@ class Namer { typer: Typer => if completedTypeParams.forall(_.isType) then completer.setCompletedTypeParams(completedTypeParams.asInstanceOf[List[TypeSymbol]]) completeTrailingParamss(ddef, sym, indexingCtor = false) - val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) + val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor, Some(ddef.nameSpan.startPos)) sym.setParamss(paramSymss) def wrapMethType(restpe: Type): Type = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 428568ecc795..c757c0a339da 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -53,6 +53,7 @@ import transform.CheckUnused.OriginalName import scala.annotation.{unchecked as _, *} import dotty.tools.dotc.util.chaining.* +import dotty.tools.dotc.ast.untpd.Mod object Typer { @@ -2883,6 +2884,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(vdef1, sym) vdef1.setDefTree + + migrate(ImplicitToGiven.valDef(vdef1)) + val nnInfo = rhs1.notNullInfo vdef1.withNotNullInfo(if sym.is(Lazy) then nnInfo.retractedInfo else nnInfo) } @@ -2995,6 +2999,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ddef2 = assignType(cpy.DefDef(ddef)(name, paramss1, tpt1, rhs1), sym) + migrate(ImplicitToGiven.defDef(ddef2)) + postProcessInfo(ddef2, sym) //todo: make sure dependent method types do not depend on implicits or by-name params } @@ -4192,6 +4198,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case wtp: MethodOrPoly => def methodStr = methPart(tree).symbol.showLocated if matchingApply(wtp, pt) then + migrate(ImplicitToGiven.implicitParams(tree, wtp, pt)) migrate(contextBoundParams(tree, wtp, pt)) migrate(implicitParams(tree, wtp, pt)) if needsTupledDual(wtp, pt) then adapt(tree, pt.tupledDual, locked) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 93238647d1a3..234da4802212 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -86,6 +86,7 @@ class CompilationTests { compileFile("tests/rewrites/i22440.scala", defaultOptions.and("-rewrite")), compileFile("tests/rewrites/i22731.scala", defaultOptions.and("-rewrite", "-source:3.7-migration")), compileFile("tests/rewrites/i22731b.scala", defaultOptions.and("-rewrite", "-source:3.7-migration")), + compileFile("tests/rewrites/implicit-to-given.scala", defaultOptions.and("-rewrite", "-Yimplicit-to-given")) ).checkRewrites() } diff --git a/tests/rewrites/implicit-to-given.check b/tests/rewrites/implicit-to-given.check new file mode 100644 index 000000000000..6375d4f303ee --- /dev/null +++ b/tests/rewrites/implicit-to-given.check @@ -0,0 +1,23 @@ +object Def: + trait A + trait B + implicit def conv1(a: A): B = ??? // should not change + given conv2: A => B = ??? + final given nonConv: A = ??? + given conv3: A => B = ??? + final given nonConv2: A = ??? + + implicit class Extension(a: Int): // should not change + def addedMethod(): A = ??? + implicit class ExtensionWithImplicit(t: String)(using a: Int): + def addedMethod(): String = ??? + class NoNonimplicitParams()(using a: Int) + + def applicationTest(using a: Int): Unit = ??? + val application = applicationTest(using 0) + val implicitArg: Int => Unit = (implicit a => applicationTest) // should not change + + given refined(): (A {type B = Int}) = ??? + + class EmptyParamListClass()(using a: Int) + def emptyParamListTest() = new EmptyParamListClass()(using 0) diff --git a/tests/rewrites/implicit-to-given.scala b/tests/rewrites/implicit-to-given.scala new file mode 100644 index 000000000000..b4ef5927ab0e --- /dev/null +++ b/tests/rewrites/implicit-to-given.scala @@ -0,0 +1,23 @@ +object Def: + trait A + trait B + implicit def conv1(a: A): B = ??? // should not change + implicit def conv2: A => B = ??? + final implicit def nonConv: A = ??? + implicit val conv3: A => B = ??? + final implicit val nonConv2: A = ??? + + implicit class Extension(a: Int): // should not change + def addedMethod(): A = ??? + implicit class ExtensionWithImplicit(t: String)(implicit a: Int): + def addedMethod(): String = ??? + class NoNonimplicitParams(implicit a: Int) + + def applicationTest(implicit a: Int): Unit = ??? + val application = applicationTest(0) + val implicitArg: Int => Unit = (implicit a => applicationTest) // should not change + + implicit def refined(): A {type B = Int} = ??? + + class EmptyParamListClass(implicit a: Int) + def emptyParamListTest() = new EmptyParamListClass()(0)