Skip to content

Commit fa42b81

Browse files
smarterlrytz
andcommitted
backend: Emit calls using the correct receiver
This is a port of scala/scala@765eb29: When emitting a virtual call, the receiver in the bytecode cannot just be the method's owner (the class in which it is declared), because that class may not be accessible at the callsite. Instead we use the type of the receiver. This was basically done to fix - aladdin bug 455 (9954eaf) - SI-1430 (0bea2ab) - basically the same bug, slightly different - SI-4283 (8707c9e) - the same for field reads In Dotty, we rarely encountered this issue because under separate compilation, we see the bridge symbols generated by javac and use their owner as the receiver, but under joint compilation of .java and .scala sources this doesn't happen, in particular this commit fixes scala#6546. It also means that we should now be able to stop creating symbols in the ClassfileParser for Java bridges, this would make joint and separate compilation more similar and thus should reduce the number of bugs which appear in one but not the other as discussed in scala#6266. After this commit, some more changes are necessary to get the updated backend to work correctly with dotty, these are implemented in the later commits of this PR. Co-Authored-By: Lukas Rytz <[email protected]>
1 parent bf283ee commit fa42b81

File tree

11 files changed

+422
-159
lines changed

11 files changed

+422
-159
lines changed

compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala

Lines changed: 145 additions & 151 deletions
Large diffs are not rendered by default.

compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import scala.tools.asm
66
import scala.annotation.switch
77
import scala.collection.mutable
88
import Primitives.{NE, EQ, TestOp, ArithmeticOp}
9+
import scala.tools.asm.tree.MethodInsnNode
910

1011
/*
1112
* A high-level facade to the ASM API for bytecode generation.
@@ -104,7 +105,7 @@ trait BCodeIdiomatic {
104105
*/
105106
abstract class JCodeMethodN {
106107

107-
def jmethod: asm.MethodVisitor
108+
def jmethod: asm.tree.MethodNode
108109

109110
import asm.Opcodes;
110111

@@ -394,21 +395,27 @@ trait BCodeIdiomatic {
394395

395396
// can-multi-thread
396397
final def invokespecial(owner: String, name: String, desc: String, itf: Boolean): Unit = {
397-
jmethod.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, desc, itf)
398+
emitInvoke(Opcodes.INVOKESPECIAL, owner, name, desc, itf)
398399
}
399400
// can-multi-thread
400401
final def invokestatic(owner: String, name: String, desc: String, itf: Boolean): Unit = {
401-
jmethod.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, desc, itf)
402+
emitInvoke(Opcodes.INVOKESTATIC, owner, name, desc, itf)
402403
}
403404
// can-multi-thread
404405
final def invokeinterface(owner: String, name: String, desc: String): Unit = {
405-
jmethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, owner, name, desc, true)
406+
emitInvoke(Opcodes.INVOKEINTERFACE, owner, name, desc, itf = true)
406407
}
407408
// can-multi-thread
408409
final def invokevirtual(owner: String, name: String, desc: String): Unit = {
409-
jmethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, name, desc, false)
410+
emitInvoke(Opcodes.INVOKEVIRTUAL, owner, name, desc, itf = false)
410411
}
411412

413+
def emitInvoke(opcode: Int, owner: String, name: String, desc: String, itf: Boolean): Unit = {
414+
val node = new MethodInsnNode(opcode, owner, name, desc, itf)
415+
jmethod.instructions.add(node)
416+
}
417+
418+
412419
// can-multi-thread
413420
final def goTo(label: asm.Label): Unit = { jmethod.visitJumpInsn(Opcodes.GOTO, label) }
414421
// can-multi-thread

compiler/src/dotty/tools/backend/jvm/BackendInterface.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,8 @@ abstract class BackendInterface extends BackendInterfaceDefinitions {
442442
}
443443

444444
abstract class SymbolHelper {
445+
def exists: Boolean
446+
445447
// names
446448
def showFullName: String
447449
def javaSimpleName: String
@@ -534,6 +536,7 @@ abstract class BackendInterface extends BackendInterfaceDefinitions {
534536
def enclosingClassSym: Symbol
535537
def originalLexicallyEnclosingClass: Symbol
536538
def nextOverriddenSymbol: Symbol
539+
def allOverriddenSymbols: List[Symbol]
537540

538541

539542
// members
@@ -603,6 +606,7 @@ abstract class BackendInterface extends BackendInterfaceDefinitions {
603606
*/
604607
def sortedMembersBasedOnFlags(required: Flags, excluded: Flags): List[Symbol]
605608
def members: List[Symbol]
609+
def decl(name: Name): Symbol
606610
def decls: List[Symbol]
607611
def underlying: Type
608612
def parents: List[Type]

compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
627627
}
628628

629629
implicit def symHelper(sym: Symbol): SymbolHelper = new SymbolHelper {
630+
def exists: Boolean = sym.exists
631+
630632
// names
631633
def showFullName: String = sym.showFullName
632634
def javaSimpleName: String = toDenot(sym).name.mangledString // addModuleSuffix(simpleName.dropLocal)
@@ -680,7 +682,7 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
680682
(sym.is(Flags.JavaStatic) || toDenot(sym).hasAnnotation(ctx.definitions.ScalaStaticAnnot))
681683
// guard against no sumbol cause this code is executed to select which call type(static\dynamic) to use to call array.clone
682684

683-
def isBottomClass: Boolean = (sym ne defn.NullClass) && (sym ne defn.NothingClass)
685+
def isBottomClass: Boolean = (sym eq defn.NullClass) || (sym eq defn.NothingClass)
684686
def isBridge: Boolean = sym.is(Flags.Bridge)
685687
def isArtifact: Boolean = sym.is(Flags.Artifact)
686688
def hasEnumFlag: Boolean = sym.isAllOf(Flags.JavaEnumTrait)
@@ -759,12 +761,14 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
759761
toDenot(sym)(shiftedContext).lexicallyEnclosingClass(shiftedContext)
760762
} else NoSymbol
761763
def nextOverriddenSymbol: Symbol = toDenot(sym).nextOverriddenSymbol
764+
def allOverriddenSymbols: List[Symbol] = toDenot(sym).allOverriddenSymbols.toList
762765

763766
// members
764767
def primaryConstructor: Symbol = toDenot(sym).primaryConstructor
765768

766769
/** For currently compiled classes: All locally defined classes including local classes.
767770
* The empty list for classes that are not currently compiled.
771+
768772
*/
769773
def nestedClasses: List[Symbol] = definedClasses(ctx.flattenPhase)
770774

@@ -876,6 +880,8 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
876880

877881
def memberInfo(s: Symbol): Type = tp.memberInfo(s)
878882

883+
def decl(name: Name): Symbol = tp.decl(name).symbol
884+
879885
def decls: List[Symbol] = tp.decls.toList
880886

881887
def members: List[Symbol] = tp.allMembers.map(_.symbol).toList

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTest.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import java.io.{File => JFile, InputStream}
2424
trait DottyBytecodeTest {
2525
import AsmNode._
2626
import ASMConverters._
27+
import DottyBytecodeTest._
2728

2829
protected object Opcode {
2930
val newarray = 188
@@ -81,6 +82,24 @@ trait DottyBytecodeTest {
8182
classNode.fields.asScala.find(_.name == name) getOrElse
8283
sys.error(s"Didn't find field '$name' in class '${classNode.name}'")
8384

85+
def getInstructions(c: ClassNode, name: String): List[Instruction] =
86+
instructionsFromMethod(getMethod(c, name))
87+
88+
def assertSameCode(method: MethodNode, expected: List[Instruction]): Unit =
89+
assertSameCode(instructionsFromMethod(method).dropNonOp, expected)
90+
def assertSameCode(actual: List[Instruction], expected: List[Instruction]): Unit = {
91+
assert(actual === expected, s"\nExpected: $expected\nActual : $actual")
92+
}
93+
94+
def assertInvoke(m: MethodNode, receiver: String, method: String): Unit =
95+
assertInvoke(instructionsFromMethod(m), receiver, method)
96+
def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = {
97+
assert(l.exists {
98+
case Invoke(_, `receiver`, `method`, _, _) => true
99+
case _ => false
100+
}, l.stringLines)
101+
}
102+
84103
def diffInstructions(isa: List[Instruction], isb: List[Instruction]): String = {
85104
val len = Math.max(isa.length, isb.length)
86105
val sb = new StringBuilder
@@ -194,3 +213,9 @@ trait DottyBytecodeTest {
194213
)
195214
}
196215
}
216+
object DottyBytecodeTest {
217+
implicit class listStringLines[T](val l: List[T]) extends AnyVal {
218+
def stringLines = l.mkString("\n")
219+
}
220+
}
221+

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import asm._
88
import asm.tree._
99

1010
import scala.tools.asm.Opcodes
11+
import scala.jdk.CollectionConverters._
12+
1113

1214
class TestBCode extends DottyBytecodeTest {
1315
import ASMConverters._
@@ -783,4 +785,134 @@ class TestBCode extends DottyBytecodeTest {
783785
assertParamName(b2, "evidence$2")
784786
}
785787
}
788+
789+
@Test
790+
def invocationReceivers(): Unit = {
791+
import Opcodes._
792+
793+
checkBCode(List(invocationReceiversTestCode.definitions("Object"))) { dir =>
794+
val c1 = loadClassNode(dir.lookupName("C1.class", directory = false).input)
795+
val c2 = loadClassNode(dir.lookupName("C2.class", directory = false).input)
796+
// Scala 2 uses "invokestatic T.clone$" here, see https://github.com/lampepfl/dotty/issues/5928
797+
assertSameCode(getMethod(c1, "clone"), List(VarOp(ALOAD, 0), Invoke(INVOKESPECIAL, "T", "clone", "()Ljava/lang/Object;", true), Op(ARETURN)))
798+
assertInvoke(getMethod(c1, "f1"), "T", "clone")
799+
assertInvoke(getMethod(c1, "f2"), "T", "clone")
800+
assertInvoke(getMethod(c1, "f3"), "C1", "clone")
801+
assertInvoke(getMethod(c2, "f1"), "T", "clone")
802+
assertInvoke(getMethod(c2, "f2"), "T", "clone")
803+
assertInvoke(getMethod(c2, "f3"), "C1", "clone")
804+
}
805+
checkBCode(List(invocationReceiversTestCode.definitions("String"))) { dir =>
806+
val c1b = loadClassNode(dir.lookupName("C1.class", directory = false).input)
807+
val c2b = loadClassNode(dir.lookupName("C2.class", directory = false).input)
808+
val tb = loadClassNode(dir.lookupName("T.class", directory = false).input)
809+
val ub = loadClassNode(dir.lookupName("U.class", directory = false).input)
810+
811+
def ms(c: ClassNode, n: String) = c.methods.asScala.toList.filter(_.name == n)
812+
assert(ms(tb, "clone").length == 1)
813+
assert(ms(ub, "clone").isEmpty)
814+
val List(c1Clone) = ms(c1b, "clone").filter(_.desc == "()Ljava/lang/Object;")
815+
assert((c1Clone.access | Opcodes.ACC_BRIDGE) != 0)
816+
assertSameCode(c1Clone, List(VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C1", "clone", "()Ljava/lang/String;", false), Op(ARETURN)))
817+
818+
def iv(m: MethodNode) = getInstructions(c1b, "f1").collect({case i: Invoke => i})
819+
assertSameCode(iv(getMethod(c1b, "f1")), List(Invoke(INVOKEINTERFACE, "T", "clone", "()Ljava/lang/String;", true)))
820+
assertSameCode(iv(getMethod(c1b, "f2")), List(Invoke(INVOKEINTERFACE, "T", "clone", "()Ljava/lang/String;", true)))
821+
// invokeinterface T.clone in C1 is OK here because it is not an override of Object.clone (different signature)
822+
assertSameCode(iv(getMethod(c1b, "f3")), List(Invoke(INVOKEINTERFACE, "T", "clone", "()Ljava/lang/String;", true)))
823+
}
824+
}
825+
826+
@Test
827+
def invocationReceiversProtected(): Unit = {
828+
// http://lrytz.github.io/scala-aladdin-bugtracker/displayItem.do%3Fid=455.html / 9954eaf
829+
// also https://github.com/scala/bug/issues/1430 / 0bea2ab (same but with interfaces)
830+
val aC =
831+
"""package a;
832+
|/*package private*/ abstract class A {
833+
| public int f() { return 1; }
834+
| public int t;
835+
|}
836+
""".stripMargin
837+
val bC =
838+
"""package a;
839+
|public class B extends A { }
840+
""".stripMargin
841+
val iC =
842+
"""package a;
843+
|/*package private*/ interface I { int f(); }
844+
""".stripMargin
845+
val jC =
846+
"""package a;
847+
|public interface J extends I { }
848+
""".stripMargin
849+
val cC =
850+
"""package b
851+
|class C {
852+
| def f1(b: a.B) = b.f
853+
| def f2(b: a.B) = { b.t = b.t + 1 }
854+
| def f3(j: a.J) = j.f
855+
|}
856+
""".stripMargin
857+
858+
checkBCode(scalaSources = List(cC), javaSources = List(aC, bC, iC, jC)) { dir =>
859+
val clsIn = dir.subdirectoryNamed("b").lookupName("C.class", directory = false).input
860+
val c = loadClassNode(clsIn)
861+
862+
assertInvoke(getMethod(c, "f1"), "a/B", "f") // receiver needs to be B (A is not accessible in class C, package b)
863+
assertInvoke(getMethod(c, "f3"), "a/J", "f") // receiver needs to be J
864+
}
865+
}
866+
867+
@Test
868+
def specialInvocationReceivers(): Unit = {
869+
val code =
870+
"""class C {
871+
| def f1(a: Array[String]) = a.clone()
872+
| def f2(a: Array[Int]) = a.hashCode()
873+
| def f3(n: Nothing) = n.hashCode()
874+
| def f4(n: Null) = n.toString()
875+
|
876+
|}
877+
""".stripMargin
878+
checkBCode(List(code)) { dir =>
879+
val c = loadClassNode(dir.lookupName("C.class", directory = false).input)
880+
881+
assertInvoke(getMethod(c, "f1"), "[Ljava/lang/String;", "clone") // array descriptor as receiver
882+
assertInvoke(getMethod(c, "f2"), "java/lang/Object", "hashCode") // object receiver
883+
assertInvoke(getMethod(c, "f3"), "java/lang/Object", "hashCode")
884+
assertInvoke(getMethod(c, "f4"), "java/lang/Object", "toString")
885+
}
886+
}
887+
}
888+
889+
object invocationReceiversTestCode {
890+
// if cloneType is more specific than Object (e.g., String), a bridge method is generated.
891+
def definitions(cloneType: String): String =
892+
s"""trait T { override def clone(): $cloneType = "hi" }
893+
|trait U extends T
894+
|class C1 extends U with Cloneable {
895+
| // The comments below are true when `cloneType` is Object.
896+
| // C1 gets a forwarder for clone that invokes T.clone. this is needed because JVM method
897+
| // resolution always prefers class members, so it would resolve to Object.clone, even if
898+
| // C1 is a subtype of the interface T which has an overriding default method for clone.
899+
|
900+
| // invokeinterface T.clone
901+
| def f1 = (this: T).clone()
902+
|
903+
| // cannot invokeinterface U.clone (NoSuchMethodError). Object.clone would work here, but
904+
| // not in the example in C2 (illegal access to protected). T.clone works in all cases and
905+
| // resolves correctly.
906+
| def f2 = (this: U).clone()
907+
|
908+
| // invokevirtual C1.clone()
909+
| def f3 = (this: C1).clone()
910+
|}
911+
|
912+
|class C2 {
913+
| def f1(t: T) = t.clone() // invokeinterface T.clone
914+
| def f2(t: U) = t.clone() // invokeinterface T.clone -- Object.clone would be illegal (protected, explained in C1)
915+
| def f3(t: C1) = t.clone() // invokevirtual C1.clone -- Object.clone would be illegal
916+
|}
917+
""".stripMargin
786918
}

compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ class InlineBytecodeTests extends DottyBytecodeTest {
341341
VarOp(ALOAD, 0),
342342
Invoke(INVOKEVIRTUAL, "Test", "given_Int", "()I", false),
343343
Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "boxToInteger", "(I)Ljava/lang/Integer;", false),
344-
Invoke(INVOKEINTERFACE, "scala/Function1", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true),
344+
Invoke(INVOKEINTERFACE, "dotty/runtime/function/JFunction1$mcZI$sp", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true),
345345
Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "unboxToBoolean", "(Ljava/lang/Object;)Z", false),
346346
Op(IRETURN)
347347
)
@@ -352,7 +352,7 @@ class InlineBytecodeTests extends DottyBytecodeTest {
352352
}
353353

354354
assert(instructions.tail == expected,
355-
"`fg was not properly inlined in `test`\n" + diffInstructions(instructions, expected))
355+
"`fg was not properly inlined in `test`\n" + diffInstructions(instructions.tail, expected))
356356

357357
}
358358
}

tests/pos-java-interop/i6546/A.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package pkg;
2+
class P {
3+
public void foo() {
4+
}
5+
}
6+
7+
public class A extends P {
8+
}

tests/pos-java-interop/i6546/B.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object B {
2+
def main(args: Array[String]): Unit = {
3+
val a = new pkg.A
4+
a.foo()
5+
}
6+
}

tests/run/invocationReceivers1.scala

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
trait T { override def clone(): Object = "hi" }
2+
trait U extends T
3+
class C1 extends U with Cloneable {
4+
// C1 gets a forwarder for clone that invokes T.clone. this is needed because JVM method
5+
// resolution always prefers class members, so it would resolve to Object.clone, even if
6+
// C1 is a subtype of the interface T which has an overriding default method for clone.
7+
8+
// invokeinterface T.clone
9+
def f1 = (this: T).clone()
10+
11+
// cannot invokeinterface U.clone (NoSuchMethodError). Object.clone would work here, but
12+
// not in the example in C2 (illegal access to protected). T.clone works in all cases and
13+
// resolves correctly.
14+
def f2 = (this: U).clone()
15+
16+
// invokevirtual C1.clone()
17+
def f3 = (this: C1).clone()
18+
}
19+
20+
class C2 {
21+
def f1(t: T) = t.clone() // invokeinterface T.clone
22+
def f2(t: U) = t.clone() // invokeinterface T.clone -- Object.clone would be illegal (protected, explained in C1)
23+
def f3(t: C1) = t.clone() // invokevirtual C1.clone -- Object.clone would be illegal
24+
}
25+
26+
object Test {
27+
def main(arg: Array[String]): Unit = {
28+
val r = new StringBuffer()
29+
val c1 = new C1
30+
r.append(c1.f1)
31+
r.append(c1.f2)
32+
r.append(c1.f3)
33+
val t = new T { }
34+
val u = new U { }
35+
val c2 = new C2
36+
r.append(c2.f1(t))
37+
r.append(c2.f1(u))
38+
r.append(c2.f1(c1))
39+
r.append(c2.f2(u))
40+
r.append(c2.f2(c1))
41+
r.append(c2.f3(c1))
42+
r.toString
43+
}
44+
}
45+

0 commit comments

Comments
 (0)