Skip to content

Commit 1dc9761

Browse files
authored
Fix overcompilation due to unstable context bound desugaring (#18280)
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: scala/scala#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.
2 parents 3e00a0d + f322b7b commit 1dc9761

File tree

25 files changed

+198
-46
lines changed

25 files changed

+198
-46
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import util.Spans._, Types._, Contexts._, Constants._, Names._, NameOps._, Flags
77
import Symbols._, StdNames._, Trees._, ContextOps._
88
import Decorators._, transform.SymUtils._
99
import Annotations.Annotation
10-
import NameKinds.{UniqueName, EvidenceParamName, DefaultGetterName, WildcardParamName}
10+
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
1111
import typer.{Namer, Checking}
1212
import util.{Property, SourceFile, SourcePosition, Chars}
1313
import config.Feature.{sourceVersion, migrateTo3, enabled}
@@ -203,10 +203,14 @@ object desugar {
203203
else vdef1
204204
end valDef
205205

206-
def makeImplicitParameters(tpts: List[Tree], implicitFlag: FlagSet, forPrimaryConstructor: Boolean = false)(using Context): List[ValDef] =
207-
for (tpt <- tpts) yield {
206+
def makeImplicitParameters(
207+
tpts: List[Tree], implicitFlag: FlagSet,
208+
mkParamName: () => TermName,
209+
forPrimaryConstructor: Boolean = false
210+
)(using Context): List[ValDef] =
211+
for (tpt, i) <- tpts.zipWithIndex yield {
208212
val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param
209-
val epname = EvidenceParamName.fresh()
213+
val epname = mkParamName()
210214
ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag)
211215
}
212216

@@ -240,17 +244,27 @@ object desugar {
240244
val DefDef(_, paramss, tpt, rhs) = meth
241245
val evidenceParamBuf = ListBuffer[ValDef]()
242246

247+
var seenContextBounds: Int = 0
243248
def desugarContextBounds(rhs: Tree): Tree = rhs match
244249
case ContextBounds(tbounds, cxbounds) =>
245250
val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit
246251
evidenceParamBuf ++= makeImplicitParameters(
247-
cxbounds, iflag, forPrimaryConstructor = isPrimaryConstructor)
252+
cxbounds, iflag,
253+
// Just like with `makeSyntheticParameter` on nameless parameters of
254+
// using clauses, we only need names that are unique among the
255+
// parameters of the method since shadowing does not affect
256+
// implicit resolution in Scala 3.
257+
mkParamName = () =>
258+
val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator.
259+
val ret = ContextBoundParamName(EmptyTermName, index)
260+
seenContextBounds += 1
261+
ret,
262+
forPrimaryConstructor = isPrimaryConstructor)
248263
tbounds
249264
case LambdaTypeTree(tparams, body) =>
250265
cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body))
251266
case _ =>
252267
rhs
253-
254268
val paramssNoContextBounds =
255269
mapParamss(paramss) {
256270
tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam.rhs))
@@ -409,11 +423,11 @@ object desugar {
409423
meth.paramss :+ evidenceParams
410424
cpy.DefDef(meth)(paramss = paramss1)
411425

412-
/** The implicit evidence parameters of `meth`, as generated by `desugar.defDef` */
426+
/** The parameters generated from the contextual bounds of `meth`, as generated by `desugar.defDef` */
413427
private def evidenceParams(meth: DefDef)(using Context): List[ValDef] =
414428
meth.paramss.reverse match {
415429
case ValDefs(vparams @ (vparam :: _)) :: _ if vparam.mods.isOneOf(GivenOrImplicit) =>
416-
vparams.takeWhile(_.name.is(EvidenceParamName))
430+
vparams.takeWhile(_.name.is(ContextBoundParamName))
417431
case _ =>
418432
Nil
419433
}
@@ -1588,7 +1602,7 @@ object desugar {
15881602

15891603
def makeContextualFunction(formals: List[Tree], body: Tree, erasedParams: List[Boolean])(using Context): Function = {
15901604
val mods = Given
1591-
val params = makeImplicitParameters(formals, mods)
1605+
val params = makeImplicitParameters(formals, mods, mkParamName = () => ContextFunctionParamName.fresh())
15921606
FunctionWithMods(params, body, Modifiers(mods), erasedParams)
15931607
}
15941608

compiler/src/dotty/tools/dotc/core/NameKinds.scala

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,31 @@ object NameKinds {
278278
if (underlying.isEmpty) "$" + info.num + "$" else super.mkString(underlying, info)
279279
}
280280

281+
/** The name of the term parameter generated for a context bound:
282+
*
283+
* def foo[T: A](...): ...
284+
*
285+
* becomes:
286+
*
287+
* def foo[T](...)(using evidence$1: A[T]): ...
288+
*
289+
* The "evidence$" prefix is a convention copied from Scala 2.
290+
*/
291+
val ContextBoundParamName: UniqueNameKind = new UniqueNameKind("evidence$")
292+
293+
/** The name of an inferred contextual function parameter:
294+
*
295+
* val x: A ?=> B = b
296+
*
297+
* becomes:
298+
*
299+
* val x: A ?=> B = (contextual$1: A) ?=> b
300+
*/
301+
val ContextFunctionParamName: UniqueNameKind = new UniqueNameKind("contextual$")
302+
281303
/** Other unique names */
304+
val CanThrowEvidenceName: UniqueNameKind = new UniqueNameKind("canThrow$")
282305
val TempResultName: UniqueNameKind = new UniqueNameKind("ev$")
283-
val EvidenceParamName: UniqueNameKind = new UniqueNameKind("evidence$")
284306
val DepParamName: UniqueNameKind = new UniqueNameKind("(param)")
285307
val LazyImplicitName: UniqueNameKind = new UniqueNameKind("$_lazy_implicit_$")
286308
val LazyLocalName: UniqueNameKind = new UniqueNameKind("$lzy")

compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ object Scala3:
216216

217217
def isEmptyNumbered: Boolean =
218218
!name.is(NameKinds.WildcardParamName)
219-
&& !name.is(NameKinds.EvidenceParamName)
219+
&& !name.is(NameKinds.ContextBoundParamName)
220+
&& !name.is(NameKinds.ContextFunctionParamName)
220221
&& { name match
221222
case NameKinds.AnyNumberedName(nme.EMPTY, _) => true
222223
case _ => false

compiler/src/dotty/tools/dotc/typer/Implicits.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Contexts._
1313
import Types._
1414
import Flags._
1515
import Mode.ImplicitsEnabled
16-
import NameKinds.{LazyImplicitName, EvidenceParamName}
16+
import NameKinds.{LazyImplicitName, ContextBoundParamName}
1717
import Symbols._
1818
import Types._
1919
import Decorators._
@@ -993,7 +993,7 @@ trait Implicits:
993993
def addendum = if (qt1 eq qt) "" else (i"\nWhere $qt is an alias of: $qt1")
994994
i"parameter of ${qual.tpe.widen}$addendum"
995995
case _ =>
996-
i"${ if paramName.is(EvidenceParamName) then "an implicit parameter"
996+
i"${ if paramName.is(ContextBoundParamName) then "a context parameter"
997997
else s"parameter $paramName" } of $methodStr"
998998
}
999999

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,8 +1734,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
17341734
checkInInlineContext("summonFrom", tree.srcPos)
17351735
val cases1 = tree.cases.mapconserve {
17361736
case cdef @ CaseDef(pat @ Typed(Ident(nme.WILDCARD), _), _, _) =>
1737-
// case _ : T --> case evidence$n : T
1738-
cpy.CaseDef(cdef)(pat = untpd.Bind(EvidenceParamName.fresh(), pat))
1737+
// case _ : T --> case _$n : T
1738+
cpy.CaseDef(cdef)(pat = untpd.Bind(WildcardParamName.fresh(), pat))
17391739
case cdef => cdef
17401740
}
17411741
typedMatchFinish(tree, tpd.EmptyTree, defn.ImplicitScrutineeTypeRef, cases1, pt)
@@ -2010,7 +2010,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20102010
def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree =
20112011
def makeCanThrow(tp: Type): untpd.Tree =
20122012
untpd.ValDef(
2013-
EvidenceParamName.fresh(),
2013+
CanThrowEvidenceName.fresh(),
20142014
untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)),
20152015
untpd.ref(defn.Compiletime_erasedValue))
20162016
.withFlags(Given | Final | Erased)
@@ -3756,7 +3756,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
37563756
else tree
37573757
else if wtp.isContextualMethod then
37583758
def isContextBoundParams = wtp.stripPoly match
3759-
case MethodType(EvidenceParamName(_) :: _) => true
3759+
case MethodType(ContextBoundParamName(_) :: _) => true
37603760
case _ => false
37613761
if sourceVersion == `future-migration` && isContextBoundParams && pt.args.nonEmpty
37623762
then // Under future-migration, don't infer implicit arguments yet for parameters

compiler/src/dotty/tools/dotc/util/Signatures.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ object Signatures {
407407
(params :: rest)
408408

409409
def isSyntheticEvidence(name: String) =
410-
if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else
410+
if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else
411411
symbol.paramSymss.flatten.find(_.name.show == name).exists(_.flags.is(Flags.Implicit))
412412

413413
denot.info.stripPoly match

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ class DottyBytecodeTests extends DottyBytecodeTest {
874874
}
875875
}
876876

877-
@Test def freshNames = {
877+
@Test def stableNames = {
878878
val sourceA =
879879
"""|class A {
880880
| def a1[T: Ordering]: Unit = {}
@@ -902,11 +902,11 @@ class DottyBytecodeTests extends DottyBytecodeTest {
902902
s"Method ${mn.name} has parameter $actualName but expected $expectedName")
903903
}
904904

905-
// The fresh name counter should be reset for every compilation unit
905+
// Each definition should get the same names since there's no possible clashes.
906906
assertParamName(a1, "evidence$1")
907-
assertParamName(a2, "evidence$2")
907+
assertParamName(a2, "evidence$1")
908908
assertParamName(b1, "evidence$1")
909-
assertParamName(b2, "evidence$2")
909+
assertParamName(b2, "evidence$1")
910910
}
911911
}
912912

presentation-compiler/src/main/dotty/tools/pc/completions/ScaladocCompletions.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ object ScaladocCompletions:
112112
defdef.trailingParamss.flatten.collect {
113113
case param
114114
if !param.symbol.isOneOf(Synthetic) &&
115-
!param.name.is(EvidenceParamName) &&
115+
!param.name.is(ContextBoundParamName) &&
116116
param.symbol != extensionParam =>
117117
param.name.show
118118
}
@@ -121,7 +121,7 @@ object ScaladocCompletions:
121121
case param
122122
if !param.is(Synthetic) &&
123123
!param.isTypeParam &&
124-
!param.name.is(EvidenceParamName) =>
124+
!param.name.is(ContextBoundParamName) =>
125125
param.name.show
126126
}
127127
case other =>

presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import scala.meta.pc.SymbolSearch
99
import dotty.tools.dotc.core.Contexts.Context
1010
import dotty.tools.dotc.core.Flags
1111
import dotty.tools.dotc.core.Flags.*
12-
import dotty.tools.dotc.core.NameKinds.EvidenceParamName
12+
import dotty.tools.dotc.core.NameKinds.ContextBoundParamName
1313
import dotty.tools.dotc.core.NameOps.*
1414
import dotty.tools.dotc.core.Names
1515
import dotty.tools.dotc.core.Names.Name
@@ -270,7 +270,7 @@ class ShortenedTypePrinter(
270270

271271
lazy val implicitEvidenceParams: Set[Symbol] =
272272
implicitParams
273-
.filter(p => p.name.toString.startsWith(EvidenceParamName.separator))
273+
.filter(p => p.name.toString.startsWith(ContextBoundParamName.separator))
274274
.toSet
275275

276276
lazy val implicitEvidencesByTypeParam: Map[Symbol, List[String]] =
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package database
2+
3+
object A {
4+
def wrapper: B.Wrapper = ???
5+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package database
2+
3+
object B {
4+
trait GetValue[T]
5+
6+
object GetValue {
7+
implicit def inst[T]: GetValue[T] = ???
8+
}
9+
10+
class ResultSet {
11+
def getV[A: GetValue]: A = ???
12+
}
13+
14+
trait DBParse[T] {
15+
def apply(rs: ResultSet): T
16+
}
17+
18+
class AVG() {
19+
def call: String = "AVG"
20+
}
21+
22+
object ClientOwnerId {
23+
class CompanyId
24+
25+
def parseClientOwnerId[T: DBParse]: Unit = {}
26+
}
27+
28+
class Wrapper(companyId: ClientOwnerId.CompanyId)
29+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package database
2+
3+
object C {
4+
def foo: Unit = {
5+
val rs: B.ResultSet = ???
6+
rs.getV[String]
7+
}
8+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
scalaVersion := sys.props("plugin.scalaVersion")
2+
3+
import sbt.internal.inc.Analysis
4+
import complete.DefaultParsers._
5+
6+
// Reset compiler iterations, necessary because tests run in batch mode
7+
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
8+
recordPreviousIterations := {
9+
val log = streams.value.log
10+
CompileState.previousIterations = {
11+
val previousAnalysis = (previousCompile in Compile).value.analysis.asScala
12+
previousAnalysis match {
13+
case None =>
14+
log.info("No previous analysis detected")
15+
0
16+
case Some(a: Analysis) => a.compilations.allCompilations.size
17+
}
18+
}
19+
}
20+
21+
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
22+
23+
checkIterations := {
24+
val expected: Int = (Space ~> NatBasic).parsed
25+
val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
26+
assert(expected == actual, s"Expected $expected compilations, got $actual")
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package database
2+
3+
object B {
4+
trait GetValue[T]
5+
6+
object GetValue {
7+
implicit def inst[T]: GetValue[T] = ???
8+
}
9+
10+
class ResultSet {
11+
def getV[A: GetValue]: A = ???
12+
}
13+
14+
trait DBParse[T]
15+
16+
class AVG() {
17+
def call: String = "AVG2"
18+
}
19+
20+
object ClientOwnerId {
21+
class CompanyId
22+
23+
def parseClientOwnerId[T: DBParse]: Unit = {}
24+
}
25+
26+
class Wrapper(companyId: ClientOwnerId.CompanyId)
27+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is necessary because tests are run in batch mode
2+
object CompileState {
3+
@volatile var previousIterations: Int = -1
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
> compile
2+
> recordPreviousIterations
3+
4+
# change only the body of a method
5+
$ copy-file changes/B.scala B.scala
6+
7+
# Only B.scala should be recompiled. Previously, this lead to a subsequent
8+
# compilation round because context bounds were desugared into names unique to
9+
# the whole compilation unit, and in the first `compile` the two context bounds
10+
# of B.scala were desugared into `evidence$2` and `evidence$1` in this order
11+
# (because the definitions were visited out of order), but in the second call
12+
# to `compile` we traverse them in order as we typecheck B.scala and ended up
13+
# with `evidence$1` and `evidence$2` instead.
14+
> compile
15+
> checkIterations 1

scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ trait ClassLikeSupport:
578578
val baseTypeRepr = typeForClass(c).memberType(symbol)
579579

580580
def isSyntheticEvidence(name: String) =
581-
if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else
581+
if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else
582582
// This assumes that every parameter that starts with `evidence$` and is implicit is generated by compiler to desugar context bound.
583583
// Howrever, this is just a heuristic, so
584584
// `def foo[A](evidence$1: ClassTag[A]) = 1`

tests/neg/i10901.check

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
| [T1, T2]
1313
| (x: BugExp4Point2D.ColumnType[T1])
1414
| (y: BugExp4Point2D.ColumnType[T2])
15-
| (implicit evidence$7: Numeric[T1], evidence$8: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
15+
| (implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
1616
| [T1, T2]
1717
| (x: T1)
1818
| (y: BugExp4Point2D.ColumnType[T2])
19-
| (implicit evidence$5: Numeric[T1], evidence$6: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
19+
| (implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
2020
| both match arguments ((x : BugExp4Point2D.IntT.type))((y : BugExp4Point2D.DoubleT.type))
2121
-- [E008] Not Found Error: tests/neg/i10901.scala:48:38 ----------------------------------------------------------------
2222
48 | val pos4: Point2D[Int,Double] = x º 201.1 // error
@@ -31,8 +31,8 @@
3131
| Ambiguous overload. The overloaded alternatives of method º in object dsl with types
3232
| [T1, T2]
3333
| (x: BugExp4Point2D.ColumnType[T1])
34-
| (y: T2)(implicit evidence$9: Numeric[T1], evidence$10: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
35-
| [T1, T2](x: T1)(y: T2)(implicit evidence$3: Numeric[T1], evidence$4: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
34+
| (y: T2)(implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
35+
| [T1, T2](x: T1)(y: T2)(implicit evidence$1: Numeric[T1], evidence$2: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
3636
| both match arguments ((x : BugExp4Point2D.IntT.type))((201.1d : Double))
3737
-- [E008] Not Found Error: tests/neg/i10901.scala:62:16 ----------------------------------------------------------------
3838
62 | val y = "abc".foo // error

0 commit comments

Comments
 (0)