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
53 changes: 43 additions & 10 deletions compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,36 @@ object GenericSignatures {

@noinline
private final def javaSig0(sym0: Symbol, info: Type)(using Context): Option[String] = {
// This works as long as mangled names are always valid valid Java identifiers,
// if we change our name encoding, we'll have to `throw new UnknownSig` here for
// names which are not valid Java identifiers (see git history of this method).
def sanitizeName(name: Name): String = name.mangledString

val builder = new StringBuilder(64)
val isTraitSignature = sym0.enclosingClass.is(Trait)

// Collect class-level type parameter names to avoid conflicts with method-level type parameters
val usedNames = collection.mutable.Set.empty[String]
if(sym0.is(Method)) {
sym0.enclosingClass.typeParams.foreach { tp =>
Copy link
Member

Choose a reason for hiding this comment

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

Is this enough? We should get the type parameters of all the enclosing classes.
The core of this issue has to do with substitution and the fact that we convert to String. I could substitute the enclosing of the enclosing class type parameters by something that will create conflicts (from a String point of view).

I would have suggested a more robust algorithm where we do a first pass to go over all the types referred to in the Signature and disambiguate if they have a different id but the same name (as a String).

Copy link
Member Author

@tanishiking tanishiking Dec 2, 2025

Choose a reason for hiding this comment

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

Is this enough? We should get the type parameters of all the enclosing classes.

Ouch, you're right. For example,

class Container[A]:
  abstract class JavaPartialFunction[B] extends PartialFunction[A, B]:

Still have a generic signature attribute having a name clash.

public abstract class Container$JavaPartialFunction<B> implements scala.PartialFunction<A, B> {
  // ...
  public <A> scala.Function1<A, B> compose(scala.Function1<A, A>);

The core of this issue has to do with substitution and the fact that we convert to String. I could substitute the enclosing of the enclosing class type parameters by something that will create conflicts (from a String point of view).
I would have suggested a more robust algorithm where we do a first pass to go over all the types referred to in the Signature and disambiguate if they have a different id but the same name (as a String).

IIUC, that's what this PR does (?), except it collects only a direct parent's type parameters though.
Do you mean, use more "robust" data structure than String for detect name shadowing? (but I feel like String is enough for detecting conflict at a String level in the end).

Anyway, I'll open the issue, and make it work for nested classes.

usedNames += sanitizeName(tp.name)
}
}
val methodTypeParamRenaming = collection.mutable.Map.empty[String, String]
def freshTypeParamName(sanitizedName: String): String = {
if !usedNames.contains(sanitizedName) then sanitizedName
else {
var i = 1
var newName = sanitizedName + i
while usedNames.contains(newName) do
i += 1
newName = sanitizedName + i
methodTypeParamRenaming(sanitizedName) = newName
usedNames += newName
newName
}
}

def superSig(cls: Symbol, parents: List[Type]): Unit = {
def isInterfaceOrTrait(sym: Symbol) = sym.is(PureInterface) || sym.is(Trait)

Expand Down Expand Up @@ -133,15 +160,16 @@ object GenericSignatures {
else
Right(parent))

def paramSig(param: TypeParamInfo): Unit = {
builder.append(sanitizeName(param.paramName.lastPart))
def tparamSig(param: TypeParamInfo): Unit = {
val freshName = freshTypeParamName(sanitizeName(param.paramName.lastPart))
builder.append(freshName)
boundsSig(hiBounds(param.paramInfo.bounds))
}

def polyParamSig(tparams: List[TypeParamInfo]): Unit =
if (tparams.nonEmpty) {
builder.append('<')
tparams.foreach(paramSig)
tparams.foreach(tparamSig)
builder.append('>')
}

Expand All @@ -151,6 +179,12 @@ object GenericSignatures {
builder.append(';')
}

def typeParamSigWithName(sanitizedName: String): Unit = {
builder.append(ClassfileConstants.TVAR_TAG)
builder.append(sanitizedName)
builder.append(';')
}

def methodResultSig(restpe: Type): Unit = {
val finalType = restpe.finalResultType
val sym = finalType.typeSymbol
Expand All @@ -160,11 +194,6 @@ object GenericSignatures {
jsig(finalType)
}

// This works as long as mangled names are always valid valid Java identifiers,
// if we change our name encoding, we'll have to `throw new UnknownSig` here for
// names which are not valid Java identifiers (see git history of this method).
def sanitizeName(name: Name): String = name.mangledString

// Anything which could conceivably be a module (i.e. isn't known to be
// a type parameter or similar) must go through here or the signature is
// likely to end up with Foo<T>.Empty where it needs Foo<T>.Empty$.
Expand Down Expand Up @@ -244,7 +273,11 @@ object GenericSignatures {
// don't emit type param name if the param is upper-bounded by a primitive type (including via a value class)
if erasedUnderlying.isPrimitiveValueType then
jsig(erasedUnderlying, toplevel = toplevel, unboxedVCs = unboxedVCs)
else typeParamSig(ref.paramName.lastPart)
else {
val name = sanitizeName(ref.paramName.lastPart)
val nameToUse = methodTypeParamRenaming.getOrElse(name, name)
typeParamSigWithName(nameToUse)
}

case ref: TermRef if ref.symbol.isGetter =>
// If the type of a val is a TermRef to another val, generating the generic signature
Expand All @@ -258,7 +291,7 @@ object GenericSignatures {

case ref: SingletonType =>
// Singleton types like `x.type` need to be widened to their underlying type
// For example, `def identity[A](x: A): x.type` should have signature
// For example, `def identity[A](x: A): x.type` should have signature
// with return type `A` (not `java.lang.Object`)
jsig(ref.underlying, toplevel = toplevel, unboxedVCs = unboxedVCs)

Expand Down
1 change: 1 addition & 0 deletions compiler/test/dotc/pos-test-pickling.excludelist
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ i17186b.scala
i11982a.scala
i17255
i17735.scala
i24134

# Tree is huge and blows stack for printing Text
i7034.scala
Expand Down
20 changes: 20 additions & 0 deletions tests/pos/i24134/JavaPartialFunction.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
final class Flow[In, Out]:
def collect[T](pf: PartialFunction[Out, T]): Flow[In, T] = ???

object Flow:
def create[T](): Flow[T, T] = ???


abstract class Message:
def asTextMessage: TextMessage

abstract class TextMessage extends Message

abstract class JavaPartialFunction[A, B] extends PartialFunction[A, B]:
@throws(classOf[Exception]) // required, compiles without annotation
def apply(x: A, isCheck: Boolean): B

final def isDefinedAt(x: A): Boolean = ???
final override def apply(x: A): B = ???
final override def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 = ???

18 changes: 18 additions & 0 deletions tests/pos/i24134/JavaTestServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
public class JavaTestServer {
public static Flow<Message, Message> greeter() {
return Flow.<Message>create()
.collect(
new JavaPartialFunction<Message, Message>() {
@Override
public Message apply(Message msg, boolean isCheck) throws Exception {
if (isCheck) throw new RuntimeException();
else return handleTextMessage(msg.asTextMessage());
}
});
}

public static TextMessage handleTextMessage(TextMessage msg) {
return null;
}
}

Loading