Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -510,19 +510,45 @@ object GenericSignatures {
}

private object RefOrAppliedType {
def unapply(tp: Type)(using Context): Option[(Symbol, Type, List[Type])] = tp match {
private enum ResolvedAppliedType:
case Resolved(t: Type)
case NotResolved
case Bail
// In the special case where we see a type parameter applied to type parameters,
// such as `K[X, Y]` given `[X, Y, K <: Iterable[(X, Y)]]`, we must find its bound
// and instantiate it, otherwise in our example we end up with `Iterable[X, Y]` which is nonsensical.
private def resolveAppliedType(a: AppliedType)(using Context): ResolvedAppliedType =
a.tycon match
case TypeParamRef(binder, paramNum) =>
binder.paramInfos(paramNum).hi match
case hkt @ HKTypeLambda(_, _) =>
val instantiated = hkt.instantiate(a.args).dealias
Copy link
Copy Markdown
Member

@tanishiking tanishiking Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note]
.dealias is required, otherwise type M[X,Y] appears in generic signature in the test below. 👍

// However, since Java doesn't have a way to refer to HKTs in generic signatures,
// we must trade precision for termination by only resolving one level,
// otherwise we end up in infinite loops,
// e.g., in `X[A] <: Thing[X[A]]` or `X[A] <: X[Thing[A]]` we keep resolving `X`.
// In that case we must completely give up on the genericity, i.e.,
// in `X[A] <: Y[X[Z[A]]]` it would not be correct to use `Y[A]` as a type signature!
if instantiated.existsPart(_ == a.tycon) then ResolvedAppliedType.Bail
else ResolvedAppliedType.Resolved(instantiated)
case _ => ResolvedAppliedType.NotResolved
case _ => ResolvedAppliedType.NotResolved

def unapply(tp: Type)(using Context): Option[(Symbol, Type, List[Type])] = tp match
case TypeParamRef(_, _) =>
Some((tp.typeSymbol, tp, Nil))
case TermParamRef(_, _) =>
Some((tp.termSymbol, tp, Nil))
case TypeRef(pre, _) if !tp.typeSymbol.isAliasType =>
val sym = tp.typeSymbol
Some((sym, pre, Nil))
case AppliedType(pre, args) =>
Some((pre.typeSymbol, pre, args))
case a @ AppliedType(pre, args) =>
resolveAppliedType(a) match
case ResolvedAppliedType.Resolved(resolved) => unapply(resolved)
case ResolvedAppliedType.NotResolved => Some((pre.typeSymbol, pre, args))
case ResolvedAppliedType.Bail => None
case _ =>
None
}
}

private def needsJavaSig(tp: Type, throwsArgs: List[Type])(using Context): Boolean = !ctx.settings.XnoGenericSig.value && {
Expand Down
1 change: 1 addition & 0 deletions tests/run/i15903.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scala.collection.Iterable<scala.Tuple2<K, A>>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we wanna move these tests into tests/generic-java-signatures instead of tests/run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the purpose of this generic-java-signatures folder exactly? we could move a lot of tests in there indeed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

files under tests/generic-java-signatures runs in separate JUnit suite

@Test def genericJavaSignatures: Unit = {
implicit val testGroup: TestGroup = TestGroup("genericJavaSignatures")
val compilationTest = withCoverage(compileFilesInDir("tests/generic-java-signatures", defaultOptions))
runWithCoverageOrFallback[RunTestWithCoverage](compilationTest, "Run")

On the other hand, files under tests/run could be belong to multiple JUnit suite, like run (with 2 different options), pickle, and recheck. That means we could run test multiple times.

So, if the test is specific for checking java generic signatures, they would be nice to go to tests/generic-java-signatures. (While I also somtimes forget about that...). And yeah, maybe we wanna move bunch of tests in there in another PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not testing multiple times is nice... also I just realized it means we don't need to explicitly add scalajs: --skip and that's already a good enough reason for me :D

I'll grep scalajs: --skip and move the ones that belong to genericJavaSignatures in another PR 👍

10 changes: 10 additions & 0 deletions tests/run/i15903.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// scalajs: --skip
// (JVM-only, generic signatures)

trait Working:
type M[X,Y] = Iterable[(X, Y)]
def example[K, A, T[X,Y] <: M[X,Y]](ab: String): T[K,A] = ???

object Test:
def main(args: Array[String]): Unit =
classOf[Working].getMethods.filter(_.getName == "example").map(_.getGenericReturnType).foreach(println)
1 change: 1 addition & 0 deletions tests/run/i15903b.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scala.collection.Iterable<scala.Tuple2<K, A>>
9 changes: 9 additions & 0 deletions tests/run/i15903b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// scalajs: --skip
// (JVM-only, generic signatures)

trait NotWorking:
def example[K, A, T[X,Y] <: Iterable[(X, Y)]](ab: String): T[K,A] = ???

object Test:
def main(args: Array[String]): Unit =
classOf[NotWorking].getMethods.filter(_.getName == "example").map(_.getGenericReturnType).foreach(println)
1 change: 1 addition & 0 deletions tests/run/i15903c.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scala.collection.Iterable<scala.Tuple2<A, K>>
10 changes: 10 additions & 0 deletions tests/run/i15903c.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// scalajs: --skip
// (JVM-only, generic signatures)

trait NotWorking:
// return type args reversed compared to test case (b)
def example[K, A, T[X,Y] <: Iterable[(X, Y)]](ab: String): T[A,K] = ???

object Test:
def main(args: Array[String]): Unit =
classOf[NotWorking].getMethods.filter(_.getName == "example").map(_.getGenericReturnType).foreach(println)
1 change: 1 addition & 0 deletions tests/run/i15903d.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
interface scala.collection.Iterable
9 changes: 9 additions & 0 deletions tests/run/i15903d.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// scalajs: --skip
// (JVM-only, generic signatures)

trait NotWorking:
def example[A, X[T] <: Iterable[X[List[T]]]](s: String): X[A] = ???

object Test:
def main(args: Array[String]): Unit =
classOf[NotWorking].getMethods.filter(_.getName == "example").map(_.getGenericReturnType).foreach(println)
Loading