Skip to content

Commit 1b6661b

Browse files
committed
SI-7015 Removes redundant aconst_null; pop; aconst_null creation
In an effort to adapt methods and field accesses of type Null to other types, we were always emitting aconst_null pop aconst_null The problem is we were doing that even when the JVM was in a position to know it had null value, e.g. when the user had written a null constant. This commit fixes that and includes a test to show that the resulting byte code still works even without repeating ourselves and/or repeating ourselves. This commit also makes the scala.runtim.Null$ constructor private. It was a sealed abstract class which prevented subclassing in Scala, but it didn't prevent subclassing in Java. A private constructor takes care of that hole so now the only value of type Null$ should be null. Along the way I found some other questionable things in adapt and I've added TODO's and issue https://issues.scala-lang.org/browse/SI-7159 to track.
1 parent 70956e5 commit 1b6661b

File tree

4 files changed

+115
-12
lines changed

4 files changed

+115
-12
lines changed

src/compiler/scala/tools/nsc/backend/icode/GenICode.scala

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ abstract class GenICode extends SubComponent {
275275
ctx1 = genLoad(args.head, ctx1, INT)
276276
generatedType = elem
277277
ctx1.bb.emit(LOAD_ARRAY_ITEM(elementType), tree.pos)
278+
// it's tempting to just drop array loads of type Null instead
279+
// of adapting them but array accesses can cause
280+
// ArrayIndexOutOfBounds so we can't. Besides, Array[Null]
281+
// probably isn't common enough to figure out an optimization
282+
adaptNullRef(generatedType, expectedType, ctx1, tree.pos)
278283
}
279284
else if (scalaPrimitives.isArraySet(code)) {
280285
debugassert(args.length == 2,
@@ -790,7 +795,9 @@ abstract class GenICode extends SubComponent {
790795
}
791796
generatedType =
792797
if (sym.isClassConstructor) UNIT
793-
else toTypeKind(sym.info.resultType);
798+
else toTypeKind(sym.info.resultType)
799+
// deal with methods that return Null
800+
adaptNullRef(generatedType, expectedType, ctx1, tree.pos)
794801
ctx1
795802
}
796803
}
@@ -842,14 +849,15 @@ abstract class GenICode extends SubComponent {
842849

843850
if (sym.isModule) {
844851
genLoadModule(genLoadQualUnlessElidable, tree)
845-
}
846-
else if (sym.isStaticMember) {
847-
val ctx1 = genLoadQualUnlessElidable
848-
ctx1.bb.emit(LOAD_FIELD(sym, true) setHostClass hostClass, tree.pos)
849-
ctx1
850852
} else {
851-
val ctx1 = genLoadQualifier(tree, ctx)
852-
ctx1.bb.emit(LOAD_FIELD(sym, false) setHostClass hostClass, tree.pos)
853+
val isStatic = sym.isStaticMember
854+
val ctx1 = if (isStatic) genLoadQualUnlessElidable
855+
else genLoadQualifier(tree, ctx)
856+
ctx1.bb.emit(LOAD_FIELD(sym, isStatic) setHostClass hostClass, tree.pos)
857+
// it's tempting to drop field accesses of type Null instead of adapting them,
858+
// but field access can cause static class init so we can't. Besides, fields
859+
// of type Null probably aren't common enough to figure out an optimization
860+
adaptNullRef(generatedType, expectedType, ctx1, tree.pos)
853861
ctx1
854862
}
855863
}
@@ -997,22 +1005,56 @@ abstract class GenICode extends SubComponent {
9971005

9981006
resCtx
9991007
}
1008+
1009+
/**
1010+
* If we have a method call, field load, or array element load of type Null then
1011+
* we need to convince the JVM that we have a null value because in Scala
1012+
* land Null is a subtype of all ref types, but in JVM land scala.runtime.Null$
1013+
* is not. Note we don't have to adapt loads of locals because the JVM type
1014+
* system for locals does have a null type which it tracks internally. As
1015+
* long as we adapt these other things, the JVM will know that a Scala local of
1016+
* type Null is holding a null.
1017+
*/
1018+
private def adaptNullRef(from: TypeKind, to: TypeKind, ctx: Context, pos: Position) {
1019+
log(s"GenICode#adaptNullRef($from, $to, $ctx, $pos)")
1020+
1021+
// Don't need to adapt null to unit because we'll just drop it anyway. Don't
1022+
// need to adapt to Object or AnyRef because the JVM is happy with
1023+
// upcasting Null to them.
1024+
// We do have to adapt from NullReference to NullReference because we could be storing
1025+
// this value into a local of type Null and we want the JVM to see that it's
1026+
// a null value so we don't have to also adapt local loads.
1027+
if (from == NullReference && to != UNIT && to != ObjectReference && to != AnyRefReference) {
1028+
assert(to.isReferenceType, "Attempt to adapt a null to a non reference type")
1029+
// adapt by dropping what we've got and pushing a null which
1030+
// will convince the JVM we really do have null
1031+
ctx.bb.emit(DROP(from), pos)
1032+
ctx.bb.emit(CONSTANT(Constant(null)), pos)
1033+
}
1034+
}
10001035

10011036
private def adapt(from: TypeKind, to: TypeKind, ctx: Context, pos: Position) {
10021037
// An awful lot of bugs explode here - let's leave ourselves more clues.
10031038
// A typical example is an overloaded type assigned after typer.
10041039
log(s"GenICode#adapt($from, $to, $ctx, $pos)")
10051040

1006-
val conforms = (from <:< to) || (from == NullReference && to == NothingReference)
1041+
val conforms = (from <:< to) || (from == NullReference && to == NothingReference) // TODO why would we have null where we expect nothing?
10071042
def coerce(from: TypeKind, to: TypeKind) = ctx.bb.emit(CALL_PRIMITIVE(Conversion(from, to)), pos)
10081043
def checkAssertions() {
10091044
def msg = s"Can't convert from $from to $to in unit ${unit.source} at $pos"
10101045
debugassert(from != UNIT, msg)
10111046
assert(!from.isReferenceType && !to.isReferenceType, msg)
10121047
}
10131048
if (conforms) from match {
1049+
// The JVM doesn't have a Nothing equivalent, so it doesn't know that a method of type Nothing can't actually return. So for instance, with
1050+
// def f: String = ???
1051+
// we need
1052+
// 0: getstatic #25; //Field scala/Predef$.MODULE$:Lscala/Predef$;
1053+
// 3: invokevirtual #29; //Method scala/Predef$.$qmark$qmark$qmark:()Lscala/runtime/Nothing$;
1054+
// 6: athrow
1055+
// So this case tacks on the ahtrow which makes the JVM happy because class Nothing is declared as a subclass of Throwable
10141056
case NothingReference => ctx.bb.emit(THROW(ThrowableClass)) ; ctx.bb.enterIgnoreMode
1015-
case NullReference => ctx.bb.emit(Seq(DROP(from), CONSTANT(Constant(null))))
1057+
// TODO why do we have this case? It's saying if we have a throwable and a non-throwable is expected then we should emit a cast? Why would we get here?
10161058
case ThrowableReference if !(ThrowableClass.tpe <:< to.toType) => ctx.bb.emit(CHECK_CAST(to)) // downcast throwables
10171059
case _ =>
10181060
// widen subrange types

src/library/scala/runtime/Null$.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package scala.runtime
1111
/**
1212
* Dummy class which exist only to satisfy the JVM. It corresponds to
1313
* `scala.Null`. If such type appears in method signatures, it is erased
14-
* to this one.
14+
* to this one. A private constructor ensures that Java code can't create
15+
* subclasses. The only value of type Null$ should be null
1516
*/
16-
sealed abstract class Null$
17+
sealed abstract class Null$ private ()

test/files/run/t7015.check

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Method returns Null type: null
2+
Method takes non Null type: null
3+
call through method null
4+
call through bridge null
5+
fetch field: null
6+
fetch field on companion: null
7+
fetch local: null
8+
fetch array element: null
9+
method that takes object: null
10+
method that takes anyref: null
11+
method that takes any: null

test/files/run/t7015.scala

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
object Test {
2+
def main(args : Array[String]) : Unit = {
3+
println(s"Method returns Null type: $f")
4+
println(s"Method takes non Null type: ${g(null)}")
5+
6+
// pass things through the g function because it expects
7+
// a string. If we haven't adapted properly then we'll
8+
// get verify errors
9+
val b = new B
10+
println(s"call through method ${g(b.f(null))}")
11+
println(s"call through bridge ${g((b: A).f(null))}")
12+
13+
println(s"fetch field: ${g(b.nullField)}")
14+
println(s"fetch field on companion: ${g(B.nullCompanionField)}")
15+
16+
val x = f
17+
println(s"fetch local: ${g(x)}")
18+
19+
val nulls = Array(f, f, f)
20+
println(s"fetch array element: ${g(nulls(0))}")
21+
22+
println(s"method that takes object: ${q(f)}")
23+
println(s"method that takes anyref: ${r(f)}")
24+
println(s"method that takes any: ${s(f)}")
25+
}
26+
27+
def f = null
28+
29+
def g(x: String) = x
30+
31+
def q(x: java.lang.Object) = x
32+
def r(x: AnyRef) = x
33+
def s(x: Any) = x
34+
}
35+
36+
abstract class A {
37+
def f(x: String): String
38+
}
39+
40+
class B extends A {
41+
val nullField = null
42+
43+
// this forces a bridge method because the return type is different
44+
override def f(x: String) : Null = null
45+
}
46+
47+
object B {
48+
val nullCompanionField = null
49+
}

0 commit comments

Comments
 (0)