From a10a271a5f341d3130c1c046df31f80c552b8ec4 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 20 Jul 2023 15:58:29 +0200 Subject: [PATCH 1/2] Use a specific NameKind for context bounds There are various places in the compiler and assorted tools where we assume that an EvidenceParamName is the name of a context bound, but in practice we also used the same NameKind in other cases such as for inferred contextual functions. This commit cleans things up by replacing EvidenceParamName by: - ContextBoundParamName - ContextFunctionParamName - CanThrowEvidenceParamName - and the existing WildcardParamName Note that Scala 2 also uses "evidence$" prefixes to represent context bounds, this is why some pretty-printing code that aims to resugar context bounds coming from both Scala 2 and 3 does a syntactic check for `ContextBoundParamName.separator` instead of a semantic check on the NameKind itself, this could perhaps be handled in a nicer way using unmangle in the Scala2Unpickler. [Cherry-picked 77be11473509f77819424c696b7a7152bd130e9e] --- .../src/dotty/tools/dotc/ast/Desugar.scala | 22 ++++++++++------- .../src/dotty/tools/dotc/core/NameKinds.scala | 24 ++++++++++++++++++- .../dotty/tools/dotc/semanticdb/Scala3.scala | 3 ++- .../dotty/tools/dotc/typer/Implicits.scala | 4 ++-- .../src/dotty/tools/dotc/typer/Typer.scala | 8 +++---- .../dotty/tools/dotc/util/Signatures.scala | 2 +- .../pc/completions/ScaladocCompletions.scala | 4 ++-- .../pc/printer/ShortenedTypePrinter.scala | 4 ++-- .../scaladoc/tasty/ClassLikeSupport.scala | 2 +- tests/neg/i11350.check | 4 ++-- tests/neg/missing-implicit1.check | 2 +- tests/neg/missing-implicit3.check | 12 +++++----- tests/neg/missing-implicit4.check | 6 ++--- tests/run-staging/multi-staging.check | 2 +- tests/run-staging/quote-nested-2.check | 2 +- tests/run-staging/quote-nested-5.check | 2 +- 16 files changed, 66 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index f0580c29e762..f11960da9783 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -7,7 +7,7 @@ import util.Spans._, Types._, Contexts._, Constants._, Names._, NameOps._, Flags import Symbols._, StdNames._, Trees._, ContextOps._ import Decorators._, transform.SymUtils._ import Annotations.Annotation -import NameKinds.{UniqueName, EvidenceParamName, DefaultGetterName, WildcardParamName} +import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} import util.{Property, SourceFile, SourcePosition, Chars} import config.Feature.{sourceVersion, migrateTo3, enabled} @@ -202,10 +202,14 @@ object desugar { else vdef1 end valDef - def makeImplicitParameters(tpts: List[Tree], implicitFlag: FlagSet, forPrimaryConstructor: Boolean = false)(using Context): List[ValDef] = - for (tpt <- tpts) yield { + def makeImplicitParameters( + tpts: List[Tree], implicitFlag: FlagSet, + mkParamName: () => TermName, + forPrimaryConstructor: Boolean = false + )(using Context): List[ValDef] = + for (tpt, i) <- tpts.zipWithIndex yield { val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param - val epname = EvidenceParamName.fresh() + val epname = mkParamName() ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag) } @@ -243,7 +247,9 @@ object desugar { case ContextBounds(tbounds, cxbounds) => val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit evidenceParamBuf ++= makeImplicitParameters( - cxbounds, iflag, forPrimaryConstructor = isPrimaryConstructor) + cxbounds, iflag, + mkParamName = () => ContextBoundParamName.fresh(), + forPrimaryConstructor = isPrimaryConstructor) tbounds case LambdaTypeTree(tparams, body) => cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body)) @@ -380,11 +386,11 @@ object desugar { meth.paramss :+ evidenceParams cpy.DefDef(meth)(paramss = paramss1) - /** The implicit evidence parameters of `meth`, as generated by `desugar.defDef` */ + /** The parameters generated from the contextual bounds of `meth`, as generated by `desugar.defDef` */ private def evidenceParams(meth: DefDef)(using Context): List[ValDef] = meth.paramss.reverse match { case ValDefs(vparams @ (vparam :: _)) :: _ if vparam.mods.isOneOf(GivenOrImplicit) => - vparams.takeWhile(_.name.is(EvidenceParamName)) + vparams.takeWhile(_.name.is(ContextBoundParamName)) case _ => Nil } @@ -1500,7 +1506,7 @@ object desugar { def makeContextualFunction(formals: List[Tree], body: Tree, erasedParams: List[Boolean])(using Context): Function = { val mods = Given - val params = makeImplicitParameters(formals, mods) + val params = makeImplicitParameters(formals, mods, mkParamName = () => ContextFunctionParamName.fresh()) FunctionWithMods(params, body, Modifiers(mods), erasedParams) } diff --git a/compiler/src/dotty/tools/dotc/core/NameKinds.scala b/compiler/src/dotty/tools/dotc/core/NameKinds.scala index 2c968ab9446c..6210553bda43 100644 --- a/compiler/src/dotty/tools/dotc/core/NameKinds.scala +++ b/compiler/src/dotty/tools/dotc/core/NameKinds.scala @@ -278,9 +278,31 @@ object NameKinds { if (underlying.isEmpty) "$" + info.num + "$" else super.mkString(underlying, info) } + /** The name of the term parameter generated for a context bound: + * + * def foo[T: A](...): ... + * + * becomes: + * + * def foo[T](...)(using evidence$1: A[T]): ... + * + * The "evidence$" prefix is a convention copied from Scala 2. + */ + val ContextBoundParamName: UniqueNameKind = new UniqueNameKind("evidence$") + + /** The name of an inferred contextual function parameter: + * + * val x: A ?=> B = b + * + * becomes: + * + * val x: A ?=> B = (contextual$1: A) ?=> b + */ + val ContextFunctionParamName: UniqueNameKind = new UniqueNameKind("contextual$") + /** Other unique names */ + val CanThrowEvidenceName: UniqueNameKind = new UniqueNameKind("canThrow$") val TempResultName: UniqueNameKind = new UniqueNameKind("ev$") - val EvidenceParamName: UniqueNameKind = new UniqueNameKind("evidence$") val DepParamName: UniqueNameKind = new UniqueNameKind("(param)") val LazyImplicitName: UniqueNameKind = new UniqueNameKind("$_lazy_implicit_$") val LazyLocalName: UniqueNameKind = new UniqueNameKind("$lzy") diff --git a/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala b/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala index e157b52fe260..d004be657fbc 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala @@ -216,7 +216,8 @@ object Scala3: def isEmptyNumbered: Boolean = !name.is(NameKinds.WildcardParamName) - && !name.is(NameKinds.EvidenceParamName) + && !name.is(NameKinds.ContextBoundParamName) + && !name.is(NameKinds.ContextFunctionParamName) && { name match case NameKinds.AnyNumberedName(nme.EMPTY, _) => true case _ => false diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 2e4e1befff66..29a35ccbdac0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -13,7 +13,7 @@ import Contexts._ import Types._ import Flags._ import Mode.ImplicitsEnabled -import NameKinds.{LazyImplicitName, EvidenceParamName} +import NameKinds.{LazyImplicitName, ContextBoundParamName} import Symbols._ import Types._ import Decorators._ @@ -975,7 +975,7 @@ trait Implicits: def addendum = if (qt1 eq qt) "" else (i"\nWhere $qt is an alias of: $qt1") i"parameter of ${qual.tpe.widen}$addendum" case _ => - i"${ if paramName.is(EvidenceParamName) then "an implicit parameter" + i"${ if paramName.is(ContextBoundParamName) then "a context parameter" else s"parameter $paramName" } of $methodStr" } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f7df01335d3d..237f48d7bd96 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1686,8 +1686,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkInInlineContext("summonFrom", tree.srcPos) val cases1 = tree.cases.mapconserve { case cdef @ CaseDef(pat @ Typed(Ident(nme.WILDCARD), _), _, _) => - // case _ : T --> case evidence$n : T - cpy.CaseDef(cdef)(pat = untpd.Bind(EvidenceParamName.fresh(), pat)) + // case _ : T --> case _$n : T + cpy.CaseDef(cdef)(pat = untpd.Bind(WildcardParamName.fresh(), pat)) case cdef => cdef } typedMatchFinish(tree, tpd.EmptyTree, defn.ImplicitScrutineeTypeRef, cases1, pt) @@ -1962,7 +1962,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree = def makeCanThrow(tp: Type): untpd.Tree = untpd.ValDef( - EvidenceParamName.fresh(), + CanThrowEvidenceName.fresh(), untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)), untpd.ref(defn.Compiletime_erasedValue)) .withFlags(Given | Final | Erased) @@ -3686,7 +3686,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else tree else if wtp.isContextualMethod then def isContextBoundParams = wtp.stripPoly match - case MethodType(EvidenceParamName(_) :: _) => true + case MethodType(ContextBoundParamName(_) :: _) => true case _ => false if sourceVersion == `future-migration` && isContextBoundParams && pt.args.nonEmpty then // Under future-migration, don't infer implicit arguments yet for parameters diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 877e02adfd7d..5fae39a20de4 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -407,7 +407,7 @@ object Signatures { (params :: rest) def isSyntheticEvidence(name: String) = - if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else + if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else symbol.paramSymss.flatten.find(_.name.show == name).exists(_.flags.is(Flags.Implicit)) denot.info.stripPoly match diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/ScaladocCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/ScaladocCompletions.scala index 35a7508f5563..81260c4df923 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/ScaladocCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/ScaladocCompletions.scala @@ -112,7 +112,7 @@ object ScaladocCompletions: defdef.trailingParamss.flatten.collect { case param if !param.symbol.isOneOf(Synthetic) && - !param.name.is(EvidenceParamName) && + !param.name.is(ContextBoundParamName) && param.symbol != extensionParam => param.name.show } @@ -121,7 +121,7 @@ object ScaladocCompletions: case param if !param.is(Synthetic) && !param.isTypeParam && - !param.name.is(EvidenceParamName) => + !param.name.is(ContextBoundParamName) => param.name.show } case other => diff --git a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala index 10978f9d33b8..7c758b464333 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala @@ -9,7 +9,7 @@ import scala.meta.pc.SymbolSearch import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.* -import dotty.tools.dotc.core.NameKinds.EvidenceParamName +import dotty.tools.dotc.core.NameKinds.ContextBoundParamName import dotty.tools.dotc.core.NameOps.* import dotty.tools.dotc.core.Names import dotty.tools.dotc.core.Names.Name @@ -270,7 +270,7 @@ class ShortenedTypePrinter( lazy val implicitEvidenceParams: Set[Symbol] = implicitParams - .filter(p => p.name.toString.startsWith(EvidenceParamName.separator)) + .filter(p => p.name.toString.startsWith(ContextBoundParamName.separator)) .toSet lazy val implicitEvidencesByTypeParam: Map[Symbol, List[String]] = diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index bf04b72328b8..1598accf4f40 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -578,7 +578,7 @@ trait ClassLikeSupport: val baseTypeRepr = typeForClass(c).memberType(symbol) def isSyntheticEvidence(name: String) = - if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else + if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else // This assumes that every parameter that starts with `evidence$` and is implicit is generated by compiler to desugar context bound. // Howrever, this is just a heuristic, so // `def foo[A](evidence$1: ClassTag[A]) = 1` diff --git a/tests/neg/i11350.check b/tests/neg/i11350.check index cf9524d36ec1..63ea4618b9c1 100644 --- a/tests/neg/i11350.check +++ b/tests/neg/i11350.check @@ -1,7 +1,7 @@ -- [E081] Type Error: tests/neg/i11350.scala:1:39 ---------------------------------------------------------------------- 1 |class A1[T](action: A1[T] ?=> String = "") // error | ^ - | Could not infer type for parameter evidence$1 of anonymous function + | Could not infer type for parameter contextual$1 of anonymous function | | Partially inferred type for the parameter: A1[] | @@ -9,7 +9,7 @@ -- [E081] Type Error: tests/neg/i11350.scala:2:39 ---------------------------------------------------------------------- 2 |class A2[T](action: A1[T] ?=> String = summon[A1[T]]) // error | ^ - | Could not infer type for parameter evidence$2 of anonymous function + | Could not infer type for parameter contextual$2 of anonymous function | | Partially inferred type for the parameter: A1[] | diff --git a/tests/neg/missing-implicit1.check b/tests/neg/missing-implicit1.check index c94225aaf0a6..6006afda3ece 100644 --- a/tests/neg/missing-implicit1.check +++ b/tests/neg/missing-implicit1.check @@ -19,7 +19,7 @@ -- [E172] Type Error: tests/neg/missing-implicit1.scala:23:42 ---------------------------------------------------------- 23 | List(1, 2, 3).traverse(x => Option(x)) // error | ^ - |No given instance of type testObjectInstance.Zip[Option] was found for an implicit parameter of method traverse in trait Traverse + |No given instance of type testObjectInstance.Zip[Option] was found for a context parameter of method traverse in trait Traverse | |The following import might fix the problem: | diff --git a/tests/neg/missing-implicit3.check b/tests/neg/missing-implicit3.check index 85201b3a772f..c58b4430f3fe 100644 --- a/tests/neg/missing-implicit3.check +++ b/tests/neg/missing-implicit3.check @@ -1,14 +1,14 @@ -- [E172] Type Error: tests/neg/missing-implicit3.scala:13:36 ---------------------------------------------------------- 13 |val sortedFoos = sort(List(new Foo)) // error | ^ - | No given instance of type ord.Ord[ord.Foo] was found for an implicit parameter of method sort in package ord. - | I found: + | No given instance of type ord.Ord[ord.Foo] was found for a context parameter of method sort in package ord. + | I found: | - | ord.Ord.ordered[ord.Foo](/* missing */summon[ord.Foo => Comparable[? >: ord.Foo]]) + | ord.Ord.ordered[ord.Foo](/* missing */summon[ord.Foo => Comparable[? >: ord.Foo]]) | - | But no implicit values were found that match type ord.Foo => Comparable[? >: ord.Foo]. + | But no implicit values were found that match type ord.Foo => Comparable[? >: ord.Foo]. | - | The following import might make progress towards fixing the problem: + | The following import might make progress towards fixing the problem: | - | import scala.math.Ordered.orderingToOrdered + | import scala.math.Ordered.orderingToOrdered | diff --git a/tests/neg/missing-implicit4.check b/tests/neg/missing-implicit4.check index e243c208ecdf..be262fc2081f 100644 --- a/tests/neg/missing-implicit4.check +++ b/tests/neg/missing-implicit4.check @@ -19,9 +19,9 @@ -- [E172] Type Error: tests/neg/missing-implicit4.scala:20:42 ---------------------------------------------------------- 20 | List(1, 2, 3).traverse(x => Option(x)) // error | ^ - | No given instance of type Zip[Option] was found for an implicit parameter of method traverse in trait Traverse + | No given instance of type Zip[Option] was found for a context parameter of method traverse in trait Traverse | - | The following import might fix the problem: + | The following import might fix the problem: | - | import instances.zipOption + | import instances.zipOption | diff --git a/tests/run-staging/multi-staging.check b/tests/run-staging/multi-staging.check index 76adcfec3034..c5f53e51a7d2 100644 --- a/tests/run-staging/multi-staging.check +++ b/tests/run-staging/multi-staging.check @@ -1,5 +1,5 @@ stage1 code: ((q1: scala.quoted.Quotes) ?=> { val x1: scala.Int = 2 - scala.quoted.runtime.Expr.quote[scala.Int](1.+(scala.quoted.runtime.Expr.splice[scala.Int](((evidence$5: scala.quoted.Quotes) ?=> scala.quoted.Expr.apply[scala.Int](x1)(scala.quoted.ToExpr.IntToExpr[scala.Int])(evidence$5))))).apply(using q1) + scala.quoted.runtime.Expr.quote[scala.Int](1.+(scala.quoted.runtime.Expr.splice[scala.Int](((contextual$5: scala.quoted.Quotes) ?=> scala.quoted.Expr.apply[scala.Int](x1)(scala.quoted.ToExpr.IntToExpr[scala.Int])(contextual$5))))).apply(using q1) }) 3 diff --git a/tests/run-staging/quote-nested-2.check b/tests/run-staging/quote-nested-2.check index 7db9edb0424e..48ecf87577ab 100644 --- a/tests/run-staging/quote-nested-2.check +++ b/tests/run-staging/quote-nested-2.check @@ -1,4 +1,4 @@ ((q: scala.quoted.Quotes) ?=> { val a: scala.quoted.Expr[scala.Int] = scala.quoted.runtime.Expr.quote[scala.Int](4).apply(using q) - ((evidence$2: scala.quoted.Quotes) ?=> a).apply(using q) + ((contextual$2: scala.quoted.Quotes) ?=> a).apply(using q) }) diff --git a/tests/run-staging/quote-nested-5.check b/tests/run-staging/quote-nested-5.check index 53600d16a8da..47d39cc92611 100644 --- a/tests/run-staging/quote-nested-5.check +++ b/tests/run-staging/quote-nested-5.check @@ -1,4 +1,4 @@ ((q: scala.quoted.Quotes) ?=> { val a: scala.quoted.Expr[scala.Int] = scala.quoted.runtime.Expr.quote[scala.Int](4).apply(using q) - ((q2: scala.quoted.Quotes) ?=> ((evidence$2: scala.quoted.Quotes) ?=> a).apply(using q2)) + ((q2: scala.quoted.Quotes) ?=> ((contextual$2: scala.quoted.Quotes) ?=> a).apply(using q2)) }.apply(using q)) From 64830a49cee19e83368e2103d47f39190ce9baf5 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 20 Jul 2023 18:04:55 +0200 Subject: [PATCH 2/2] Fix overcompilation due to unstable context bound desugaring Context bounds are desugared into term parameters `evidence$N` and before this commit, the `N` was chosen to be unique in the current compilation unit. This isn't great because it means that adding a new definition with a context bound in the middle of a file would change the desugaring of subsequent definitions in the same file. Even worse, when using incremental compilation we could end up with the same context bound desugared with a different value of `N` on different compilation runs because the order in which a compilation unit is traversed during Typer is not fixed but depends on the how the units that are jointly compiled depend on each other (as demonstrated by the `stable-ctx-bounds` test). This issue affects all fresh names generated during Typer, but it is especially problematic for context bounds because they're part of the API and renaming a method parameter forces the recompilation of all files calling that method. To fix this, we now only require context bounds parameters to have unique names among all the parameters of the method. This matches how we already desugar `def foo(using A, B)` into `def foo(using x$1: A, x$2: B)` regardless of the context. Note that fresh names used in other situations are still problematic for deterministic compilation. Most of the time they're not part of the API checked by Zinc, but they can still lead to overcompilation if they appear in an `inline def` since the entire body of the `inline def` constitutes its API. In the future, we should follow Scala 2's lead and only require names to be fresh at the method level: https://github.com/scala/scala/pull/6300 (The Scala 2 logic is slightly more complex to handle macros, but I don't think that applies to Scala 3 macros), see #7661. Fixes #18080. [Cherry-picked f322b7b3221eb3231c5a37b583827f9f80f3c66f] --- .../src/dotty/tools/dotc/ast/Desugar.scala | 12 ++++++-- .../backend/jvm/DottyBytecodeTests.scala | 8 ++--- .../stable-ctx-bounds/A.scala | 5 ++++ .../stable-ctx-bounds/B.scala | 29 +++++++++++++++++++ .../stable-ctx-bounds/C.scala | 8 +++++ .../stable-ctx-bounds/build.sbt | 27 +++++++++++++++++ .../stable-ctx-bounds/changes/B.scala | 27 +++++++++++++++++ .../project/CompileState.scala | 4 +++ .../stable-ctx-bounds/test | 15 ++++++++++ tests/neg/i10901.check | 8 ++--- 10 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/A.scala create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/B.scala create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/C.scala create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/build.sbt create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/changes/B.scala create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/stable-ctx-bounds/test diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index f11960da9783..3fcca3b73d6c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -243,19 +243,27 @@ object desugar { val DefDef(_, paramss, tpt, rhs) = meth val evidenceParamBuf = ListBuffer[ValDef]() + var seenContextBounds: Int = 0 def desugarContextBounds(rhs: Tree): Tree = rhs match case ContextBounds(tbounds, cxbounds) => val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit evidenceParamBuf ++= makeImplicitParameters( cxbounds, iflag, - mkParamName = () => ContextBoundParamName.fresh(), + // Just like with `makeSyntheticParameter` on nameless parameters of + // using clauses, we only need names that are unique among the + // parameters of the method since shadowing does not affect + // implicit resolution in Scala 3. + mkParamName = () => + val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator. + val ret = ContextBoundParamName(EmptyTermName, index) + seenContextBounds += 1 + ret, forPrimaryConstructor = isPrimaryConstructor) tbounds case LambdaTypeTree(tparams, body) => cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body)) case _ => rhs - val paramssNoContextBounds = mapParamss(paramss) { tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam.rhs)) diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index ac4ba3ee0e75..70d3c72500b2 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -874,7 +874,7 @@ class DottyBytecodeTests extends DottyBytecodeTest { } } - @Test def freshNames = { + @Test def stableNames = { val sourceA = """|class A { | def a1[T: Ordering]: Unit = {} @@ -902,11 +902,11 @@ class DottyBytecodeTests extends DottyBytecodeTest { s"Method ${mn.name} has parameter $actualName but expected $expectedName") } - // The fresh name counter should be reset for every compilation unit + // Each definition should get the same names since there's no possible clashes. assertParamName(a1, "evidence$1") - assertParamName(a2, "evidence$2") + assertParamName(a2, "evidence$1") assertParamName(b1, "evidence$1") - assertParamName(b2, "evidence$2") + assertParamName(b2, "evidence$1") } } diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/A.scala b/sbt-test/source-dependencies/stable-ctx-bounds/A.scala new file mode 100644 index 000000000000..67dd2ff18205 --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/A.scala @@ -0,0 +1,5 @@ +package database + +object A { + def wrapper: B.Wrapper = ??? +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/B.scala b/sbt-test/source-dependencies/stable-ctx-bounds/B.scala new file mode 100644 index 000000000000..ab203de92ce5 --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/B.scala @@ -0,0 +1,29 @@ +package database + +object B { + trait GetValue[T] + + object GetValue { + implicit def inst[T]: GetValue[T] = ??? + } + + class ResultSet { + def getV[A: GetValue]: A = ??? + } + + trait DBParse[T] { + def apply(rs: ResultSet): T + } + + class AVG() { + def call: String = "AVG" + } + + object ClientOwnerId { + class CompanyId + + def parseClientOwnerId[T: DBParse]: Unit = {} + } + + class Wrapper(companyId: ClientOwnerId.CompanyId) +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/C.scala b/sbt-test/source-dependencies/stable-ctx-bounds/C.scala new file mode 100644 index 000000000000..1379e9e87b4a --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/C.scala @@ -0,0 +1,8 @@ +package database + +object C { + def foo: Unit = { + val rs: B.ResultSet = ??? + rs.getV[String] + } +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/build.sbt b/sbt-test/source-dependencies/stable-ctx-bounds/build.sbt new file mode 100644 index 000000000000..bc65e91e91d7 --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/build.sbt @@ -0,0 +1,27 @@ +scalaVersion := sys.props("plugin.scalaVersion") + +import sbt.internal.inc.Analysis +import complete.DefaultParsers._ + +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + val log = streams.value.log + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis.asScala + previousAnalysis match { + case None => + log.info("No previous analysis detected") + 0 + case Some(a: Analysis) => a.compilations.allCompilations.size + } + } +} + +val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") + +checkIterations := { + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/changes/B.scala b/sbt-test/source-dependencies/stable-ctx-bounds/changes/B.scala new file mode 100644 index 000000000000..3b3cd69ea17d --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/changes/B.scala @@ -0,0 +1,27 @@ +package database + +object B { + trait GetValue[T] + + object GetValue { + implicit def inst[T]: GetValue[T] = ??? + } + + class ResultSet { + def getV[A: GetValue]: A = ??? + } + + trait DBParse[T] + + class AVG() { + def call: String = "AVG2" + } + + object ClientOwnerId { + class CompanyId + + def parseClientOwnerId[T: DBParse]: Unit = {} + } + + class Wrapper(companyId: ClientOwnerId.CompanyId) +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/project/CompileState.scala b/sbt-test/source-dependencies/stable-ctx-bounds/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/stable-ctx-bounds/test b/sbt-test/source-dependencies/stable-ctx-bounds/test new file mode 100644 index 000000000000..b42e64920bd5 --- /dev/null +++ b/sbt-test/source-dependencies/stable-ctx-bounds/test @@ -0,0 +1,15 @@ +> compile +> recordPreviousIterations + +# change only the body of a method +$ copy-file changes/B.scala B.scala + +# Only B.scala should be recompiled. Previously, this lead to a subsequent +# compilation round because context bounds were desugared into names unique to +# the whole compilation unit, and in the first `compile` the two context bounds +# of B.scala were desugared into `evidence$2` and `evidence$1` in this order +# (because the definitions were visited out of order), but in the second call +# to `compile` we traverse them in order as we typecheck B.scala and ended up +# with `evidence$1` and `evidence$2` instead. +> compile +> checkIterations 1 diff --git a/tests/neg/i10901.check b/tests/neg/i10901.check index e055bed7dd3a..4a8fa5db28bf 100644 --- a/tests/neg/i10901.check +++ b/tests/neg/i10901.check @@ -12,11 +12,11 @@ | [T1, T2] | (x: BugExp4Point2D.ColumnType[T1]) | (y: BugExp4Point2D.ColumnType[T2]) - | (implicit evidence$7: Numeric[T1], evidence$8: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | (implicit evidence$1: Numeric[T1], evidence$2: 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] + | (implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] | both match arguments ((x : BugExp4Point2D.IntT.type))((y : BugExp4Point2D.DoubleT.type)) -- [E008] Not Found Error: tests/neg/i10901.scala:48:38 ---------------------------------------------------------------- 48 | val pos4: Point2D[Int,Double] = x º 201.1 // error @@ -31,8 +31,8 @@ | 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] + | (y: T2)(implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] + | [T1, T2](x: T1)(y: T2)(implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2] | both match arguments ((x : BugExp4Point2D.IntT.type))((201.1d : Double)) -- [E008] Not Found Error: tests/neg/i10901.scala:62:16 ---------------------------------------------------------------- 62 | val y = "abc".foo // error