Skip to content

Commit 64830a4

Browse files
smarterKordyjan
authored andcommitted
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: 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. [Cherry-picked f322b7b]
1 parent a10a271 commit 64830a4

File tree

10 files changed

+133
-10
lines changed

10 files changed

+133
-10
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,19 +243,27 @@ object desugar {
243243
val DefDef(_, paramss, tpt, rhs) = meth
244244
val evidenceParamBuf = ListBuffer[ValDef]()
245245

246+
var seenContextBounds: Int = 0
246247
def desugarContextBounds(rhs: Tree): Tree = rhs match
247248
case ContextBounds(tbounds, cxbounds) =>
248249
val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit
249250
evidenceParamBuf ++= makeImplicitParameters(
250251
cxbounds, iflag,
251-
mkParamName = () => ContextBoundParamName.fresh(),
252+
// Just like with `makeSyntheticParameter` on nameless parameters of
253+
// using clauses, we only need names that are unique among the
254+
// parameters of the method since shadowing does not affect
255+
// implicit resolution in Scala 3.
256+
mkParamName = () =>
257+
val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator.
258+
val ret = ContextBoundParamName(EmptyTermName, index)
259+
seenContextBounds += 1
260+
ret,
252261
forPrimaryConstructor = isPrimaryConstructor)
253262
tbounds
254263
case LambdaTypeTree(tparams, body) =>
255264
cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body))
256265
case _ =>
257266
rhs
258-
259267
val paramssNoContextBounds =
260268
mapParamss(paramss) {
261269
tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam.rhs))

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

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

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)