Skip to content

Commit d02cbe1

Browse files
committed
Make sure Getters doesn't change signatures
Getters will replace `val x: Int`, by `def x: Int`, before this commit, this lead to the signature of `x` changing from `Signature.NotAMethod` to `Signature(Nil, scala.Int)` but we need signatures to be stable pre-erasure to disambiguate overloads. This commit fixes this by changing ExprType#signature to always return `NotAMethod`, this should not break anything since we do not allow overloads which only differ in their result type anyway. This commit also adds tests to make sure that the invariants related to signatures are not violated, currently there is one class of failure related to enums: > run -Ycheck:all tests/run/enum-constrs.scala [...] checking tests/run/enum-constrs.scala after phase MegaPhase{completeJavaEnums, elimRepeated, expandSAMs, protectedAccessors, extmethods, cacheAliasImplicits, byNameClosures, hoistSuperArgs, classOf, refchecks} exception occurred while compiling tests/run/enum-constrs.scala java.lang.AssertionError: assertion failed: Signature of constructor Color changed at phase Ycheck Initial info: (): Color Initial sig : Signature(List(),Color) Current info: (_$name: String, _$ordinal: Int): Color Current sig : Signature(List(java.lang.String, scala.Int),Color) while compiling tests/run/enum-constrs.scala The problem is that the phase CompleteJavaEnums changes the signature of enum constructors and runs before erasure, thus violating the invariant specified in the comment above Compiler#phases. I tried simply moving CompleteJavaEnums after Erasure, but that currently fails with: -- Error: tests/run/enum-constrs.scala:2:7 ------------------------------------- 2 | case Red, Green, Blue | ^ |wrong number of arguments at getClass for (_$name: String, _$ordinal: Int): Color: ($anon.super.<init> : (_$name: String, _$ordinal: Int): Color), expected: 2, found: 0 So, some more work is required here.
1 parent ab40a6e commit d02cbe1

File tree

5 files changed

+80
-9
lines changed

5 files changed

+80
-9
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ object Signature {
141141
export MatchDegree._
142142

143143
/** The signature of everything that's not a method, i.e. that has
144-
* a type different from PolyType, MethodType, or ExprType.
144+
* a type different from PolyType or MethodType.
145145
*/
146146
val NotAMethod: Signature = Signature(List(), EmptyTypeName)
147147

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3088,7 +3088,7 @@ object Types {
30883088
override def resultType(implicit ctx: Context): Type = resType
30893089
override def underlying(implicit ctx: Context): Type = resType
30903090

3091-
def computeSignature(implicit ctx: Context): Signature = resultSignature
3091+
def computeSignature(implicit ctx: Context): Signature = Signature.NotAMethod
30923092

30933093
def derivedExprType(resType: Type)(implicit ctx: Context): ExprType =
30943094
if (resType eq this.resType) this else ExprType(resType)

compiler/src/dotty/tools/dotc/transform/TreeChecker.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ class TreeChecker extends Phase with SymTransformer {
8787

8888
checkCompanion(symd)
8989

90+
// Signatures are used to disambiguate overloads and need to stay stable
91+
// until erasure, see the comment above `Compiler#phases`.
92+
if (ctx.phaseId <= ctx.erasurePhase.id) {
93+
val cur = symd.info
94+
val initial = symd.initial.info
95+
assert(cur.signature == initial.signature,
96+
i"""Signature of $symd changed at phase ${ctx.phase}
97+
|Initial info: ${initial}
98+
|Initial sig : ${initial.signature}
99+
|Current info: ${cur}
100+
|Current sig : ${cur.signature}""")
101+
}
102+
90103
symd
91104
}
92105

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dotty.tools
2+
3+
import vulpix.TestConfiguration
4+
5+
import org.junit.Test
6+
7+
import dotc.ast.Trees._
8+
import dotc.core.Decorators._
9+
import dotc.core.Contexts._
10+
import dotc.core.Types._
11+
12+
import java.io.File
13+
import java.nio.file._
14+
15+
class SignatureTest:
16+
@Test def signatureCaching: Unit =
17+
inCompilerContext(TestConfiguration.basicClasspath, separateRun = true, "case class Foo(value: Unit)") {
18+
val (ref, refSig) = ctx.atPhase(ctx.erasurePhase.next) {
19+
val cls = ctx.requiredClass("Foo")
20+
val ref = cls.requiredMethod("value").termRef
21+
(ref, ref.signature)
22+
}
23+
ctx.atPhase(ctx.typerPhase) {
24+
// NamedType#signature is always computed before erasure, which ensures
25+
// that it stays stable and therefore can be cached as long as
26+
// signatures are guaranteed to be stable before erasure, see the
27+
// comment above `Compiler#phases`.
28+
assert(refSig == ref.signature,
29+
s"""The signature of a type should never change but the signature of $ref was:
30+
|${ref.signature} at typer, whereas it was:
31+
|${refSig} after erasure""".stripMargin)
32+
assert(ref.signature == ref.denot.signature,
33+
s"""Before erasure, the signature of a TypeRef should be the signature of its denotation,
34+
|but the cached signature of $ref was:
35+
|${ref.signature}, whereas its denotation signature at typer was:
36+
|${ref.denot.signature}""".stripMargin)
37+
}
38+
}

compiler/test/dotty/tools/compilerSupport.scala

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dotty.tools
22

33
import javax.tools._
4-
import java.io.File
4+
import java.io._
55
import java.nio.file._
66
import java.net.URI
77
import scala.jdk.CollectionConverters._
@@ -10,14 +10,34 @@ import core._
1010
import core.Contexts._
1111
import dotc.core.Comments.{ContextDoc, ContextDocstrings}
1212

13-
/** Initialize a compiler context with the given classpath and
14-
* pass it to `op`.
13+
// TODO: refactor, copy-pasted from Run#compileFromStrings
14+
private def sourceFile(source: String, isJava: Boolean): util.SourceFile = {
15+
val uuid = java.util.UUID.randomUUID().toString
16+
val ext = if (isJava) ".java" else ".scala"
17+
val virtualFile = new io.VirtualFile(s"compileFromString-$uuid.$ext")
18+
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8")) // buffering is still advised by javadoc
19+
writer.write(source)
20+
writer.close()
21+
new util.SourceFile(virtualFile, scala.io.Codec.UTF8)
22+
}
23+
24+
/** Initialize a compiler context with the given `classpath`, compile all
25+
* `scalaSources`, then run `op`.
26+
*
27+
* If `separateRun` is true , then `op` will be run in a new run different from the
28+
* one used to compile the sources, this makes it easier to test for potential
29+
* issues involving retrieving symbols defined in a previous run.
1530
*/
16-
def inCompilerContext[T](classpath: String)(op: Context ?=> T): T =
31+
def inCompilerContext[T](classpath: String, separateRun: Boolean = true, scalaSources: String*)(op: Context ?=> T): T =
1732
val compiler = Compiler()
18-
val run = compiler.newRun(initCtx(classpath))
19-
run.compileUnits(Nil) // Initialize phases
20-
op(using run.runContext)
33+
val rootCtx = initCtx(classpath)
34+
val firstRun = compiler.newRun(rootCtx)
35+
firstRun.compileUnits(scalaSources.toList.map(s =>
36+
CompilationUnit(sourceFile(s, isJava = false))(using firstRun.runContext)))
37+
val opRun = if separateRun
38+
then compiler.newRun(rootCtx)
39+
else firstRun
40+
op(using opRun.runContext)
2141

2242
private def initCtx(classpath: String): Context =
2343
val ctx0 = (new ContextBase).initialCtx.fresh

0 commit comments

Comments
 (0)