Skip to content

Commit

Permalink
SI-3452 Correct Java generic signatures for mixins, static forwarders
Browse files Browse the repository at this point in the history
[Parts of this patch and some of the commentary are from @paulp]

This took me so long to figure out I can't even tell you. Partly because
there were two different bugs, one which only arose for trait forwarders
and one for mirror class forwarders, and every time I'd make one set
of tests work another set would start failing. The runtime failures
associated with these bugs were fairly well hidden because you usually
have to go through java to encounter them: scala doesn't pay that much
attention to generic signatures, so they can be wrong and scala might still
generate correct code. But java is not so lucky.

Bug #1)

During mixin composition, classes which extend traits receive forwarders
to the implementations. An attempt was made to give these the correct
info (in method "cloneBeforeErasure") but it was prone to giving
the wrong answer, because: the key attribute which the forwarder
must capture is what the underlying method will erase to *where the
implementation is*, not how it appears to the class which contains it.
That means the signature of the forwarder must be no more precise than
the signature of the inherited implementation unless additional measures
will be taken.

This subtle difference will put on an unsubtle show for you in test
run/t3452.scala.

    trait C[T]
    trait Search[M] { def search(input: M): C[Int] = null }
    object StringSearch extends Search[String] { }
    StringSearch.search("test");  // java
    // java.lang.NoSuchMethodError: StringSearch.search(Ljava/lang/String;)LC;

The principled thing to do here would be to create a pair of
methods in the host class: a mixin forwarder with the erased
signature `(String)C[Int]`, and a bridge method with the same
erased signature as the trait interface facet.

But, this turns out to be pretty hard to retrofit onto the
current setup of Mixin and Erasure, mostly due to the fact
that mixin happens after erasure which has already taken
care of bridging.

For a future, release, we should try to move all bridging
after mixin, and pursue this approach. But for now, what can
we do about `LinkageError`s for Java clients?

This commit simply checks if the pre-erasure method signature
that we generate for the trait forward erases identically to
that of the interface method. If so, we can be precise. If not,
we emit the erased signature as the generic signature.

Bug #2) The same principle is at work, at a different location.
During genjvm, objects without declared companion classes
are given static forwarders in the corresponding class, e.g.

    object Foo { def bar = 5 }

which creates these classes (taking minor liberties):

    class Foo$ { static val MODULE$ = new Foo$ ; def bar = 5 }
    class Foo  { static def bar = Foo$.MODULE$.bar }

In generating these, genjvm circumvented the usual process whereby one
creates a symbol and gives it an info, preferring to target the bytecode
directly. However generic signatures are calculated from symbol info
(in this case reusing the info from the module class.) Lacking even the
attempt which was being made in mixin to "clone before erasure", we
would have runtime failures of this kind:

    abstract class Foo {
      type T
      def f(x: T): List[T] = List()
    }
    object Bar extends Foo { type T = String }
    Bar.f("");    // java
    // java.lang.NoSuchMethodError: Bar.f(Ljava/lang/String;)Lscala/collection/immutable/List;

Before/after this commit:

    <   signature                                     f  (Ljava/lang/String;)Lscala/collection/immutable/List<Ljava/lang/String;>;
    ---
    >   signature                                     f  (Ljava/lang/Object;)Lscala/collection/immutable/List<Ljava/lang/Object;>;

This takes the warning count for compiling collections under
`-Ycheck:jvm` from 1521 to 26.
  • Loading branch information
retronym committed Feb 9, 2014
1 parent 3306967 commit 2da5b03
Show file tree
Hide file tree
Showing 31 changed files with 717 additions and 72 deletions.
13 changes: 12 additions & 1 deletion src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,18 @@ abstract class GenASM extends SubComponent with BytecodeWriters with GenJVMASM {
)

// TODO needed? for(ann <- m.annotations) { ann.symbol.initialize }
val jgensig = if (m.isDeferred) null else getGenericSignature(m, module); // only add generic signature if method concrete; bug #1745
val jgensig = (
// only add generic signature if method concrete; bug #1745
if (m.isDeferred) null else {
val clazz = module.linkedClassOfClass
val m1 = (
if ((clazz.info member m.name) eq NoSymbol)
enteringErasure(m.cloneSymbol(clazz, Flags.METHOD | Flags.STATIC))
else m
)
getGenericSignature(m1, clazz)
}
)
addRemoteExceptionAnnot(isRemoteClass, hasPublicBitSet(flags), m)
val (throws, others) = m.annotations partition (_.symbol == ThrowsClass)
val thrownExceptions: List[String] = getExceptions(throws)
Expand Down
131 changes: 76 additions & 55 deletions src/compiler/scala/tools/nsc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,30 +452,18 @@ abstract class Erasure extends AddInterfaces
val other = high
val otpe = highErased

val bridgeNeeded = exitingErasure (
!member.isMacro &&
!(other.tpe =:= member.tpe) &&
!(deconstMap(other.tpe) =:= deconstMap(member.tpe)) &&
{ var e = bridgesScope.lookupEntry(member.name)
while ((e ne null) && !((e.sym.tpe =:= otpe) && (bridgeTarget(e.sym) == member)))
e = bridgesScope.lookupNextEntry(e)
(e eq null)
}
)
val bridgeNeeded = isBridgeNeeded(pair) && {
var e = bridgesScope.lookupEntry(member.name)
while ((e ne null) && !((e.sym.tpe =:= otpe) && (bridgeTarget(e.sym) == member)))
e = bridgesScope.lookupNextEntry(e)
(e eq null)
}

if (!bridgeNeeded)
return

val newFlags = (member.flags | BRIDGE | ARTIFACT) & ~(ACCESSOR | DEFERRED | LAZY | lateDEFERRED)
val bridge = other.cloneSymbolImpl(root, newFlags) setPos root.pos

debuglog("generating bridge from %s (%s): %s to %s: %s".format(
other, flagsToString(newFlags),
otpe + other.locationString, member,
specialErasure(root)(member.tpe) + member.locationString)
)
val bridge = makeBridgeSymbol(root, pair)

// the parameter symbols need to have the new owner
bridge setInfo (otpe cloneInfo bridge)
bridgeTarget(bridge) = member

if (!(member.tpe exists (_.typeSymbol.isDerivedValueClass)) ||
Expand All @@ -487,46 +475,79 @@ abstract class Erasure extends AddInterfaces
}

bridgesScope enter bridge
bridges ::= makeBridgeDefDef(bridge, member, other)
bridges ::= makeBridgeDefDef(bridge, pair)
}
}

def makeBridgeDefDef(bridge: Symbol, member: Symbol, other: Symbol) = exitingErasure {
// type checking ensures we can safely call `other`, but unless `member.tpe <:< other.tpe`,
// calling `member` is not guaranteed to succeed in general, there's
// nothing we can do about this, except for an unapply: when this subtype test fails,
// return None without calling `member`
//
// TODO: should we do this for user-defined unapplies as well?
// does the first argument list have exactly one argument -- for user-defined unapplies we can't be sure
def maybeWrap(bridgingCall: Tree): Tree = {
val guardExtractor = ( // can't statically know which member is going to be selected, so don't let this depend on member.isSynthetic
(member.name == nme.unapply || member.name == nme.unapplySeq)
&& !exitingErasure((member.tpe <:< other.tpe))) // no static guarantees (TODO: is the subtype test ever true?)

import CODE._
val _false = FALSE
val pt = member.tpe.resultType
lazy val zero =
if (_false.tpe <:< pt) _false
else if (NoneModule.tpe <:< pt) REF(NoneModule)
else EmptyTree

if (guardExtractor && (zero ne EmptyTree)) {
val typeTest = gen.mkIsInstanceOf(REF(bridge.firstParam), member.tpe.params.head.tpe)
IF (typeTest) THEN bridgingCall ELSE zero
} else bridgingCall
}
val rhs = member.tpe match {
case MethodType(Nil, ConstantType(c)) => Literal(c)
case _ =>
val sel: Tree = Select(This(root), member)
val bridgingCall = (sel /: bridge.paramss)((fun, vparams) => Apply(fun, vparams map Ident))
def makeBridgeDefDef(bridge: Symbol, pair: SymbolPair): Tree =
Erasure.this.makeBridgeDefDef(root, bridge, pair)
}

maybeWrap(bridgingCall)
}
DefDef(bridge, rhs)
final def isBridgeNeeded(pair: SymbolPair): Boolean = {
val member = pair.low
val other = pair.high
exitingErasure(
!member.isMacro
&& !(other.tpe =:= member.tpe)
&& !(deconstMap(other.tpe) =:= deconstMap(member.tpe))
)
}

final def makeBridgeSymbol(root: Symbol, pair: SymbolPair): Symbol = {
import pair._
val member = low
val other = high
val otpe = highErased
val newFlags = (member.flags | BRIDGE | ARTIFACT) & ~(ACCESSOR | DEFERRED | LAZY | lateDEFERRED)
val bridge = pair.low.cloneSymbolImpl(root, newFlags) setPos root.pos

debuglog("generating bridge from %s (%s): %s to %s: %s".format(
other, flagsToString(newFlags),
otpe + other.locationString, member,
specialErasure(root)(member.tpe) + member.locationString)
)

// the parameter symbols need to have the new owner
bridge setInfo (otpe cloneInfo bridge)
}

final def makeBridgeDefDef(root: Symbol, bridge: Symbol, pair: SymbolPair): DefDef = exitingErasure {
val member = pair.low
val other = pair.high
// type checking ensures we can safely call `other`, but unless `member.tpe <:< other.tpe`,
// calling `member` is not guaranteed to succeed in general, there's
// nothing we can do about this, except for an unapply: when this subtype test fails,
// return None without calling `member`
//
// TODO: should we do this for user-defined unapplies as well?
// does the first argument list have exactly one argument -- for user-defined unapplies we can't be sure
def maybeWrap(bridgingCall: Tree): Tree = {
val guardExtractor = ( // can't statically know which member is going to be selected, so don't let this depend on member.isSynthetic
(member.name == nme.unapply || member.name == nme.unapplySeq)
&& !exitingErasure((member.tpe <:< other.tpe))) // no static guarantees (TODO: is the subtype test ever true?)

import CODE._
val _false = FALSE
val pt = member.tpe.resultType
lazy val zero =
if (_false.tpe <:< pt) _false
else if (NoneModule.tpe <:< pt) REF(NoneModule)
else EmptyTree

if (guardExtractor && (zero ne EmptyTree)) {
val typeTest = gen.mkIsInstanceOf(REF(bridge.firstParam), member.tpe.params.head.tpe)
IF (typeTest) THEN bridgingCall ELSE zero
} else bridgingCall
}
val rhs = member.tpe match {
case MethodType(Nil, ConstantType(c)) => Literal(c)
case _ =>
val sel: Tree = Select(This(root), member)
val bridgingCall = (sel /: bridge.paramss)((fun, vparams) => Apply(fun, vparams map Ident))

maybeWrap(bridgingCall)
}
DefDef(bridge, rhs)
}

/** The modifier typer which retypes with erased types. */
Expand Down
27 changes: 16 additions & 11 deletions src/compiler/scala/tools/nsc/transform/Mixin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,23 @@ abstract class Mixin extends InfoTransform with ast.TreeDSL {
// info) as they are seen from the class. We can't use the member that we get from the
// implementation class, as it's a clone that was made after erasure, and thus it does not
// know its info at the beginning of erasure anymore.
// Optimize: no need if mixinClass has no typeparams.
mixinMember cloneSymbol clazz modifyInfo (info =>
if (mixinClass.typeParams.isEmpty) info
else (clazz.thisType baseType mixinClass) memberInfo mixinMember
)
val sym = mixinMember cloneSymbol clazz

val erasureMap = erasure.erasure(mixinMember)
val erasedInterfaceInfo: Type = erasureMap(mixinMember.info)
val specificForwardInfo = (clazz.thisType baseType mixinClass) memberInfo mixinMember
val forwarderInfo =
if (erasureMap(specificForwardInfo) =:= erasedInterfaceInfo)
specificForwardInfo
else {
erasedInterfaceInfo
}
// Optimize: no need if mixinClass has no typeparams.
// !!! JZ Really? What about the effect of abstract types, prefix?

This comment has been minimized.

Copy link
@paulp

paulp Feb 9, 2014

Yeah, I don't know about the truth of that. I could tell that was my phrasing but I didn't remember thinking it, so I dug up the commit from whence it was paraphrased: it's 3a1d34e .

if (mixinClass.typeParams.isEmpty) sym
else sym modifyInfo (_ => forwarderInfo)
}
// clone before erasure got rid of type info we'll need to generate a javaSig
// now we'll have the type info at (the beginning of) erasure in our history,
// and now newSym has the info that's been transformed to fit this period
// (no need for asSeenFrom as phase.erasedTypes)
// TODO: verify we need the updateInfo and document why
newSym updateInfo (mixinMember.info cloneInfo newSym)
newSym
}

/** Add getters and setters for all non-module fields of an implementation
Expand Down
6 changes: 5 additions & 1 deletion test/files/neg/t4749.check
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ t4749.scala:26: warning: Fail6 has a main method with parameter type Array[Strin

object Fail6 {
^
t4749.scala:35: warning: Fail7 has a main method with parameter type Array[String], but bippy.Fail7 will not be a runnable program.
Reason: main methods cannot refer to type parameters or abstract types.
object Fail7 extends FailBippy[Unit] { }
^
error: No warnings can be incurred under -Xfatal-warnings.
6 warnings found
7 warnings found
one error found
8 changes: 4 additions & 4 deletions test/files/neg/t4749.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ package bippy {
class Fail6 {
def main = "bippy"
}
trait FailBippy[T] {
def main(args: Array[String]): T = null.asInstanceOf[T]
}
object Fail7 extends FailBippy[Unit] { }

object Win1 {
def main(args: Array[String]): Unit = ()
}
object Win2 extends Bippy[Unit] {
override def main(args: Array[String]): Unit = ()
}
trait WinBippy[T] {
def main(args: Array[String]): T = null.asInstanceOf[T]
}
object Win3 extends WinBippy[Unit] { }
}

10 changes: 10 additions & 0 deletions test/files/pos/t3452f.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Base[Coll] {
trait Transformed[S] {
lazy val underlying: Coll = ???
}
}

class Derived extends Base[String] {
class C extends Transformed[Any]
}

59 changes: 59 additions & 0 deletions test/files/run/mixin-signatures.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class Test$bar1$ {
public java.lang.String Test$bar1$.f(java.lang.Object)
public java.lang.Object Test$bar1$.f(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar1$.g(java.lang.String)
public java.lang.Object Test$bar1$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar1$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar1$.h(java.lang.Object)
}

class Test$bar2$ {
public java.lang.Object Test$bar2$.f(java.lang.String)
public java.lang.Object Test$bar2$.f(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar2$.g(java.lang.String)
public java.lang.Object Test$bar2$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar2$.g(java.lang.String) <bridge> <synthetic>
public java.lang.Object Test$bar2$.h(java.lang.Object)
}

class Test$bar3$ {
public java.lang.String Foo3.f(java.lang.Object)
generic: public java.lang.String Foo3.f(T)
public java.lang.Object Foo3.f(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar3$.g(java.lang.String)
public java.lang.Object Test$bar3$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar3$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Foo3.h(java.lang.Object)
}

class Test$bar4$ {
public java.lang.Object Foo4.f(java.lang.String)
generic: public R Foo4.f(java.lang.String)
public java.lang.Object Foo4.f(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar4$.g(java.lang.String)
public java.lang.Object Test$bar4$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar4$.g(java.lang.String) <bridge> <synthetic>
public java.lang.Object Foo4.h(java.lang.Object)
}

class Test$bar5$ {
public java.lang.String Test$bar5$.f(java.lang.String)
public java.lang.Object Test$bar5$.f(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar5$.f(java.lang.String) <bridge> <synthetic>
public java.lang.String Test$bar5$.f(java.lang.Object) <bridge> <synthetic>
public java.lang.String Test$bar5$.g(java.lang.String)
public java.lang.Object Test$bar5$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar5$.g(java.lang.String) <bridge> <synthetic>
public java.lang.String Test$bar5$.g(java.lang.Object) <bridge> <synthetic>
public java.lang.Object Test$bar5$.h(java.lang.Object)
}

class Foo1$class {
public static java.lang.String Foo1$class.f(Foo1,java.lang.Object)
}

class Foo2$class {
public static java.lang.Object Foo2$class.f(Foo2,java.lang.String)
}

000000000000000000000000000000000000
Loading

0 comments on commit 2da5b03

Please sign in to comment.