Skip to content

Handle HKT bounds in Java generic signatures#25744

Merged
SolalPirelli merged 3 commits intoscala:mainfrom
dotty-staging:solal/15903
Apr 21, 2026
Merged

Handle HKT bounds in Java generic signatures#25744
SolalPirelli merged 3 commits intoscala:mainfrom
dotty-staging:solal/15903

Conversation

@SolalPirelli
Copy link
Copy Markdown
Contributor

Fixes #15903

How much have you relied on LLM-based tools in this contribution?

zero much

How was the solution tested?

issue tests (+ 1 case inverting the arg order)

@SolalPirelli SolalPirelli requested a review from tanishiking April 9, 2026 13:01
@SolalPirelli SolalPirelli added the needs-squashing PR whose commits should be squashed by the author or via the "Squash and Merge" button label Apr 10, 2026
Copy link
Copy Markdown
Member

@tanishiking tanishiking left a comment

Choose a reason for hiding this comment

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

Overall approach looks good, but hasNested doesn't seem capture the case where expanded type's type arguments has different shape.

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]]` we keep resolving `X -> Thing[X[A]]`.
val hasNested = instantiated.existsPart:
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.

[note]

def f[A, X[T] <: Iterable[X[T]]]: X[A] = ???

this resolves to
X[A] -> Iterable[X[A]] -> Iterable[Iterable[X[A]]] -> ...

We detect X[A] in the next level (Iterable[X[A]]) and if we found a same structure in 1-level resolved signature, stop resolving.

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.

[important]
Oh wait, but what if we have slightly different resolved type signature like:

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

X[A] -> Iterable[X[List[A]] which doesn't captured by hasNested. It ends up infinite loop.

We should stop if we found X[...] with what ever type arguments.

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.

thanks, that's indeed more correct and also simpler

Comment thread tests/run/i15903.check
@@ -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 👍

Comment thread tests/run/i15903d.check Outdated
@@ -0,0 +1 @@
scala.collection.Iterable<A>
Copy link
Copy Markdown
Member

@lrytz lrytz Apr 20, 2026

Choose a reason for hiding this comment

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

Is this right? If I expand X[A] once to its bound I get Iterable[X[List[A]], which doesn't look like Iterable[A]. It's not something changed / introduced by this PR, but worth a ticket?

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.

No, indeed, that's not right... what should we have instead? Iterable as a raw type?

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.

part of the problem is I don't know what the spec of these things is

is still true 16 years later, I guess (scala/bug#3249)...

A raw type is probably sound? Or a wildcard argument scala.collection.Iterable<*>?

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.

I tried understanding what these things are supposed to be, but in another recent PR I ran into a javac assert on what I believed to be a valid signature, so I stopped trying. Let's go with a raw type here as long as tests pass.

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.

I agree with @lrytz, from X[A] <: Iterable[X[List[A]]], the best approximation seems Iterable<?>. A raw Iterable is ok, but it loses more generic information. If javac / signature accepts it (I'm wondering when javac assert for wildcard 🤔) ideally we should have Iterable<?> here.

but I think it's good to go with raw type, and open another issue to track this?

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.

opened #25887

Copy link
Copy Markdown
Member

@tanishiking tanishiking left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!
I think we wanna move tests to tests/generic-java-signatures, but that can be done in another PR with other test files all together.

Comment thread tests/run/i15903.check
@@ -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.

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.

Comment thread tests/run/i15903d.check Outdated
@@ -0,0 +1 @@
scala.collection.Iterable<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.

I agree with @lrytz, from X[A] <: Iterable[X[List[A]]], the best approximation seems Iterable<?>. A raw Iterable is ok, but it loses more generic information. If javac / signature accepts it (I'm wondering when javac assert for wildcard 🤔) ideally we should have Iterable<?> here.

but I think it's good to go with raw type, and open another issue to track this?

@SolalPirelli SolalPirelli merged commit 3e2bc79 into scala:main Apr 21, 2026
45 checks passed
@SolalPirelli SolalPirelli deleted the solal/15903 branch April 21, 2026 07:09
mbovel pushed a commit to mbovel/dotty that referenced this pull request May 4, 2026
Fixes scala#15903

## How much have you relied on LLM-based tools in this contribution?

zero much

## How was the solution tested?

issue tests (+ 1 case inverting the arg order)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-squashing PR whose commits should be squashed by the author or via the "Squash and Merge" button

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MalformedParameterizedTypeException in getGenericReturnType for T[X,Y] <: Iterable[(X, Y)]

3 participants