From c576680f266ec3505a811374e40b957f53592a3f Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 10 Aug 2018 14:44:40 -0400 Subject: [PATCH 001/127] Make Null a subtype of only Any and AnyRef This is the initial change disconnecting Null from the bottom of the type hierarchy. Modified multiple places in the compiler where the notion that Null is a subtype of any reference type is hardcoded. At this point, there are many failing tests and the compiler can no longer bootstrap. --- .../src/dotty/tools/backend/jvm/DottyBackendInterface.scala | 2 +- compiler/src/dotty/tools/dotc/core/Definitions.scala | 6 ++---- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 6 +++--- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala index 781885faec65..5e1454c65fe4 100644 --- a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala +++ b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala @@ -685,7 +685,7 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma ((sym is Flags.JavaStatic) || (owner is Flags.ImplClass) || toDenot(sym).hasAnnotation(ctx.definitions.ScalaStaticAnnot)) // guard against no sumbol cause this code is executed to select which call type(static\dynamic) to use to call array.clone - def isBottomClass: Boolean = (sym ne defn.NullClass) && (sym ne defn.NothingClass) + def isBottomClass: Boolean = sym ne defn.NothingClass def isBridge: Boolean = sym is Flags.Bridge def isArtifact: Boolean = sym is Flags.Artifact def hasEnumFlag: Boolean = sym is Flags.JavaEnum diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 24c7f5910f59..7c574a70d369 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -962,10 +962,8 @@ class Definitions { name.length > prefix.length && name.drop(prefix.length).forall(_.isDigit)) - def isBottomClass(cls: Symbol): Boolean = - cls == NothingClass || cls == NullClass - def isBottomType(tp: Type): Boolean = - tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass) + def isBottomClass(cls: Symbol) = cls == NothingClass + def isBottomType(tp: Type) = tp.derivesFrom(NothingClass) /** Is a function class. * - FunctionN for N >= 0 diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 9e08f4c4f821..5b98be795645 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -680,9 +680,9 @@ object SymDenotations { // after Erasure and to avoid cyclic references caused by forcing denotations } - /** Is this symbol a class references to which that are supertypes of null? */ + /** Is this symbol a class with nullable values? */ final def isNullableClass(implicit ctx: Context): Boolean = - isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass + symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass /** Is this definition accessible as a member of tree with type `pre`? * @param pre The type of the tree from which the selection is made @@ -1549,7 +1549,7 @@ object SymDenotations { derivesFrom(base) || base.isClass && ( (symbol eq defn.NothingClass) || - (symbol eq defn.NullClass) && (base ne defn.NothingClass)) + (symbol eq defn.NullClass) && base.isNullableClass) final override def typeParamCreationFlags: FlagSet = ClassTypeParamCreationFlags diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index aef040c5df78..a1763bd8f859 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -852,7 +852,7 @@ trait Implicits { self: Typer => else if (rtp.isRef(defn.NullClass)) ltp else NoType - (other ne NoType) && !other.derivesFrom(defn.AnyValClass) + (other ne NoType) && other.classSymbol.isNullableClass } // Map all non-opaque abstract types to their upper bound. From 2a22e7a2e3e1b49b00d337acbc5179bbfadf8900 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 21 Aug 2018 13:11:54 -0400 Subject: [PATCH 002/127] Automatically replace "null" by "???" in positive tests The two replaced cases are: * ascriptions: (null: String) => (??? : String) * vals/vars: val x: String = null => val x: String = ??? --- tests/pos-scala2/i3396.scala | 2 +- tests/pos/Transactions.scala | 2 +- tests/pos/capturedVars.scala | 2 +- tests/pos/compound.scala | 6 +++--- tests/pos/devalify.scala | 2 +- tests/pos/exbound.scala | 2 +- tests/pos/extractor-types.scala | 8 ++++---- tests/pos/flow.scala | 2 +- tests/pos/hklub0.scala | 4 ++-- tests/pos/i1352.scala | 2 +- tests/pos/i2378.scala | 4 ++-- tests/pos/i2554.scala | 2 +- tests/pos/i262-null-subtyping.scala | 14 +++++++------- tests/pos/i2749.scala | 2 +- tests/pos/i2948.scala | 2 +- tests/pos/i342.scala | 2 +- tests/pos/i3606.scala | 4 ++-- tests/pos/i536.scala | 2 +- tests/pos/localmodules.scala | 2 +- tests/pos/overloaded_ho_fun.scala | 4 ++-- tests/pos/pat_iuli.scala | 2 +- tests/pos/refinedSubtyping.scala | 4 ++-- tests/pos/sams.scala | 2 +- tests/pos/spec-doubledef-old.scala | 2 +- tests/pos/t0091.scala | 2 +- tests/pos/t0904.scala | 4 ++-- tests/pos/t0905.scala | 2 +- tests/pos/t1053.scala | 2 +- tests/pos/t151.scala | 2 +- tests/pos/t1560.scala | 2 +- tests/pos/t2023.scala | 2 +- tests/pos/t2261.scala | 2 +- tests/pos/t2712-6.scala | 2 +- tests/pos/t3528.scala | 2 +- tests/pos/t359.scala | 8 ++++---- tests/pos/t443.scala | 6 +++--- tests/pos/t4579.scala | 2 +- tests/pos/t5240.scala | 2 +- tests/pos/t5359.scala | 2 +- tests/pos/t5399.scala | 4 ++-- tests/pos/t5399a.scala | 4 ++-- tests/pos/t5683.scala | 8 ++++---- tests/pos/t5720-ownerous.scala | 2 +- tests/pos/t5829.scala | 2 +- tests/pos/t7369.scala | 4 ++-- tests/pos/t7694.scala | 4 ++-- tests/pos/t7983.scala | 4 ++-- tests/pos/t8177d.scala | 2 +- tests/pos/test4.scala | 8 ++++---- tests/pos/test4a.scala | 2 +- tests/pos/test4refine.scala | 8 ++++---- tests/pos/test5.scala | 4 ++-- tests/pos/test5refine.scala | 4 ++-- tests/pos/testcast.scala | 6 +++--- tests/pos/virtpatmat_exist2.scala | 4 ++-- tests/pos/virtpatmat_exist4.scala | 2 +- 56 files changed, 97 insertions(+), 97 deletions(-) diff --git a/tests/pos-scala2/i3396.scala b/tests/pos-scala2/i3396.scala index 5cf3ccba8980..2091f6547976 100644 --- a/tests/pos-scala2/i3396.scala +++ b/tests/pos-scala2/i3396.scala @@ -23,7 +23,7 @@ object Test { def main(args: Array[String]): Unit = { - implicit val taggedInt: Tagged[Int] = null + implicit val taggedInt: Tagged[Int] = ??? assert(implicitly[Foo[Int]].value) // fooDefault diff --git a/tests/pos/Transactions.scala b/tests/pos/Transactions.scala index dc33e8c377a2..eee146c813eb 100644 --- a/tests/pos/Transactions.scala +++ b/tests/pos/Transactions.scala @@ -25,7 +25,7 @@ class Transaction { var id: Long = _ // only for real transactions var head: Transaction = this - var next: Transaction = null + var next: Transaction = ??? def this(hd: Transaction, tl: Transaction) = { this(); this.head = head; this.next = next } diff --git a/tests/pos/capturedVars.scala b/tests/pos/capturedVars.scala index 2cbcf111ae99..21020c930b59 100644 --- a/tests/pos/capturedVars.scala +++ b/tests/pos/capturedVars.scala @@ -7,7 +7,7 @@ class Test { var x: Int = 1 var y: String = "abc" @volatile var vx: Double = 2 - @volatile var vo: Exception = null + @volatile var vo: Exception = ??? var xs: Array[Int] = Array(1, 2, 3) val xs1: Object = xs diff --git a/tests/pos/compound.scala b/tests/pos/compound.scala index 24a936f13e01..4ef457374370 100644 --- a/tests/pos/compound.scala +++ b/tests/pos/compound.scala @@ -3,12 +3,12 @@ abstract class A { type T } abstract class B { val xz: Any } abstract class Test { - var yy: A with B { type T; val xz: T } = null; - var xx: A with B { type T; val xz: T } = null; + var yy: A with B { type T; val xz: T } = ???; + var xx: A with B { type T; val xz: T } = ???; xx = yy; } abstract class Test2 { - var yy: A with B { type T; val xz: T } = null; + var yy: A with B { type T; val xz: T } = ???; val xx: A with B { type T; val xz: T } = yy } diff --git a/tests/pos/devalify.scala b/tests/pos/devalify.scala index b4e4a848cc7f..b85580af6e36 100644 --- a/tests/pos/devalify.scala +++ b/tests/pos/devalify.scala @@ -3,7 +3,7 @@ object Test { trait I { def foo: Any = null } - val s: I = null + val s: I = ??? s.foo } diff --git a/tests/pos/exbound.scala b/tests/pos/exbound.scala index be78abc269de..13a03a22024b 100644 --- a/tests/pos/exbound.scala +++ b/tests/pos/exbound.scala @@ -3,5 +3,5 @@ class A[T <: A[T]] { } object Test { - val x: A[_] = null + val x: A[_] = ??? } diff --git a/tests/pos/extractor-types.scala b/tests/pos/extractor-types.scala index 200279be6ffe..377abb5e03ce 100644 --- a/tests/pos/extractor-types.scala +++ b/tests/pos/extractor-types.scala @@ -1,6 +1,6 @@ package p1 { - object Ex { def unapply(p: Any): Option[_ <: Int] = null } - object Foo { val Ex(_) = null } + object Ex { def unapply(p: Any): Option[_ <: Int] = ??? } + object Foo { val Ex(_) = ??? } } // a.scala:2: error: error during expansion of this match (this is a scalac bug). // The underlying error was: type mismatch; @@ -17,8 +17,8 @@ package p2 { } trait Reifiers { def f(): Unit = { - val u2: Other = null - (null: Any) match { case u2.Baz(x) => println(x) } //: u2.Quux) } + val u2: Other = ??? + (??? : Any) match { case u2.Baz(x) => println(x) } //: u2.Quux) } // The underlying error was: type mismatch; // found : Other#Quux // required: u2.Quux diff --git a/tests/pos/flow.scala b/tests/pos/flow.scala index 76c0d372c8a3..4ebf03c7102e 100644 --- a/tests/pos/flow.scala +++ b/tests/pos/flow.scala @@ -9,7 +9,7 @@ trait Flow[-In, +Out] extends FlowOps[Out] { class Test { def slowFlow: Unit = { - (null: Flow[String, String]) + (??? : Flow[String, String]) .map(b => b) .map(b => b) .map(b => b) diff --git a/tests/pos/hklub0.scala b/tests/pos/hklub0.scala index 36cd46332c28..48facaad08b7 100644 --- a/tests/pos/hklub0.scala +++ b/tests/pos/hklub0.scala @@ -1,5 +1,5 @@ object Test { - val a : scala.collection.generic.GenericCompanion[scala.collection.immutable.Seq] = null - val b : scala.collection.generic.GenericCompanion[scala.collection.mutable.Seq] = null + val a: scala.collection.generic.GenericCompanion[scala.collection.immutable.Seq] = ??? + val b: scala.collection.generic.GenericCompanion[scala.collection.mutable.Seq] = ??? List(a, b) // immutable.this.List.apply[scala.collection.generic.GenericCompanion[Seq]](Test.this.a, Test.this.b) } diff --git a/tests/pos/i1352.scala b/tests/pos/i1352.scala index b73ef33fc2cd..83aa2d212982 100644 --- a/tests/pos/i1352.scala +++ b/tests/pos/i1352.scala @@ -5,7 +5,7 @@ object Test { class Foo extends Parent with A class Bar extends Parent with B - (null: Parent) match { + (??? : Parent) match { case (_: A) | (_: B) => /* * This case would incorrectly be reported as an error, diff --git a/tests/pos/i2378.scala b/tests/pos/i2378.scala index 26e95207c270..ac9a764f50cb 100644 --- a/tests/pos/i2378.scala +++ b/tests/pos/i2378.scala @@ -17,7 +17,7 @@ trait Toolbox { class Test(val tb: Toolbox) { import tb._ - implicit val cap: Cap = null + implicit val cap: Cap = ??? def foo(tree: Tree): Int = tree match { case Apply(fun, args) => 3 @@ -26,4 +26,4 @@ class Test(val tb: Toolbox) { def bar(tree: tpd.Tree): Int = tree match { case Apply(fun, args) => 3 } -} \ No newline at end of file +} diff --git a/tests/pos/i2554.scala b/tests/pos/i2554.scala index f8a77019c3fd..6ee4973b63d8 100644 --- a/tests/pos/i2554.scala +++ b/tests/pos/i2554.scala @@ -12,7 +12,7 @@ object foo { } object Test { import foo._ - implicit val shape: Shape[_ <: FlatShapeLevel, Int, Int, _] = null + implicit val shape: Shape[_ <: FlatShapeLevel, Int, Int, _] = ??? def hint = Shape.tuple2Shape(shape, shape) val hint2: foo.Shape[foo.FlatShapeLevel, (Int, Int), (Int, Int), _] = hint } diff --git a/tests/pos/i262-null-subtyping.scala b/tests/pos/i262-null-subtyping.scala index 5e57fcca0a8f..ac36f94d5653 100644 --- a/tests/pos/i262-null-subtyping.scala +++ b/tests/pos/i262-null-subtyping.scala @@ -1,16 +1,16 @@ object O { trait Base extends Any { type T } - val a: Base { type T } = null; - val b: Any with Base { type T } = null; + val a: Base { type T } = ???; + val b: Any with Base { type T } = ???; - val c: AnyRef with Base { type T } = null; + val c: AnyRef with Base { type T } = ???; class A class B - val d: A & B = null - val e: A | B = null + val d: A & B = ??? + val e: A | B = ??? - val f: (A & B) { def toString: String } = null - val g: (A | B) { def toString: String } = null + val f: (A & B) { def toString: String } = ??? + val g: (A | B) { def toString: String } = ??? } diff --git a/tests/pos/i2749.scala b/tests/pos/i2749.scala index 5d4972421ebb..0e494c209cc2 100644 --- a/tests/pos/i2749.scala +++ b/tests/pos/i2749.scala @@ -8,7 +8,7 @@ object Test { object Test2 { val f: implicit (implicit Int => Char) => Boolean = ??? - implicit val s: String = null + implicit val s: String = ??? implicit val g: implicit Int => implicit String => Char = ??? f : Boolean diff --git a/tests/pos/i2948.scala b/tests/pos/i2948.scala index 0dac0f5284be..61c5eb995f7d 100644 --- a/tests/pos/i2948.scala +++ b/tests/pos/i2948.scala @@ -1,5 +1,5 @@ import scala.collection.mutable.ListBuffer class Foo { - val zipped: ListBuffer[(String, Int)] = null + val zipped: ListBuffer[(String, Int)] = ??? val unzipped: (ListBuffer[String], ListBuffer[Int]) = zipped.unzip } diff --git a/tests/pos/i342.scala b/tests/pos/i342.scala index bb57bae8e231..7bf7d72a1b87 100644 --- a/tests/pos/i342.scala +++ b/tests/pos/i342.scala @@ -1,6 +1,6 @@ object Test { def test2: Int = { - var ds: String = null + var ds: String = ??? def s = { ds = "abs" ds diff --git a/tests/pos/i3606.scala b/tests/pos/i3606.scala index 88e5c278ea55..6b24ec8bc1f2 100644 --- a/tests/pos/i3606.scala +++ b/tests/pos/i3606.scala @@ -1,7 +1,7 @@ object Test { def foo: Unit = { - val a: GenericCompanion2[Bar] = null - val b: GenericCompanion2[Baz] = null + val a: GenericCompanion2[Bar] = ??? + val b: GenericCompanion2[Baz] = ??? List(a, b) } } diff --git a/tests/pos/i536.scala b/tests/pos/i536.scala index f2b8f9ce6b28..49d8f6058be3 100644 --- a/tests/pos/i536.scala +++ b/tests/pos/i536.scala @@ -2,7 +2,7 @@ trait Comp[T] trait Coll[T] class C extends Comp[C] object Max { - def max[M <: Comp[_ >: M]](x: Coll[_ <: M]): M = ??? + def max[M <: Comp[_ >: M]](x: Coll[_ <: M] | Null): M = ??? def max[M](x: Coll[_ <: M], cmp: Object): M = ??? val xs: Coll[C] = ??? val m1 = max(xs) diff --git a/tests/pos/localmodules.scala b/tests/pos/localmodules.scala index 3e1600842c20..da77150dbf06 100644 --- a/tests/pos/localmodules.scala +++ b/tests/pos/localmodules.scala @@ -16,7 +16,7 @@ object main { def main(args: Array[String]) = { val aa = new a; - val xx: aa.b.c = null; + val xx: aa.b.c = ???; Console.println(aa.bar(xx)); } } diff --git a/tests/pos/overloaded_ho_fun.scala b/tests/pos/overloaded_ho_fun.scala index fba1363e60e5..bf7740547944 100644 --- a/tests/pos/overloaded_ho_fun.scala +++ b/tests/pos/overloaded_ho_fun.scala @@ -62,8 +62,8 @@ object SI10194 { trait Z[A] extends Y[A] - (null: Y[Int]).map(x => x.toString) // compiled - (null: Z[Int]).map(x => x.toString) // didn't compile + (??? : Y[Int]).map(x => x.toString) // compiled + (??? : Z[Int]).map(x => x.toString) // didn't compile } // Perform eta-expansion of methods passed as functions to overloaded functions diff --git a/tests/pos/pat_iuli.scala b/tests/pos/pat_iuli.scala index 46356ff58812..bfcec8f99c9b 100644 --- a/tests/pos/pat_iuli.scala +++ b/tests/pos/pat_iuli.scala @@ -9,7 +9,7 @@ trait Ops { self: MyCodes => trait Blox { self: MyCodes => import opcodes._ class Basick { - var foo: Instru = null + var foo: Instru = ??? def bar = foo match { case SWITCH(i) => i diff --git a/tests/pos/refinedSubtyping.scala b/tests/pos/refinedSubtyping.scala index e6a972e1c6a8..d65b5ab5bd73 100644 --- a/tests/pos/refinedSubtyping.scala +++ b/tests/pos/refinedSubtyping.scala @@ -64,8 +64,8 @@ class Test4 { abstract class A { type T; val xz: Any } - val yy: A { val xz: T } = null; -// val xx: A { val xz: T } = null; + val yy: A { val xz: T } = ???; +// val xx: A { val xz: T } = ???; val zz: A { val xz: T } = yy; } diff --git a/tests/pos/sams.scala b/tests/pos/sams.scala index e18ff81ec805..d150e73d5eaa 100644 --- a/tests/pos/sams.scala +++ b/tests/pos/sams.scala @@ -82,7 +82,7 @@ class Foo[T] { } object Foo { - val f: Foo[Int] = null + val f: Foo[Int] = ??? val m = f.toMap(_ % 2) } } diff --git a/tests/pos/spec-doubledef-old.scala b/tests/pos/spec-doubledef-old.scala index bde259e4facc..f61d1ad026ab 100644 --- a/tests/pos/spec-doubledef-old.scala +++ b/tests/pos/spec-doubledef-old.scala @@ -18,7 +18,7 @@ abstract class B[T, @specialized(scala.Int) U : Manifest, @specialized(scala.Int val v: V def f(t: T, v2: V): Tuple2[U, V] = { - val m: Array[U] = null + val m: Array[U] = ??? if (m.isEmpty) { (u, v) } else { diff --git a/tests/pos/t0091.scala b/tests/pos/t0091.scala index 414e2c931ab7..1ec750317065 100644 --- a/tests/pos/t0091.scala +++ b/tests/pos/t0091.scala @@ -1,6 +1,6 @@ class Bug { def main(args: Array[String]) = { - var msg: String = null; + var msg: String = ???; val f: PartialFunction[Any, Unit] = { case 42 => msg = "coucou" }; } } diff --git a/tests/pos/t0904.scala b/tests/pos/t0904.scala index 28ad30fc2dde..d66cf721ab6e 100644 --- a/tests/pos/t0904.scala +++ b/tests/pos/t0904.scala @@ -6,8 +6,8 @@ trait A { trait B extends A abstract class Foo { - val a: A = null - val b: B = null + val a: A = ??? + val b: B = ??? a(0) = 1 b(0) = 1 diff --git a/tests/pos/t0905.scala b/tests/pos/t0905.scala index 3800c6e0ba10..fff6aee8a41f 100644 --- a/tests/pos/t0905.scala +++ b/tests/pos/t0905.scala @@ -1,6 +1,6 @@ object Test { trait A[T] def f(implicit p: A[_]) = null - implicit val x: A[_] = null + implicit val x: A[_] = ??? println(f) } diff --git a/tests/pos/t1053.scala b/tests/pos/t1053.scala index 2c5dc1d5a9d8..fe122777fa47 100644 --- a/tests/pos/t1053.scala +++ b/tests/pos/t1053.scala @@ -2,6 +2,6 @@ trait T[A] { trait U { type W = A; val x = 3 } } trait Base { type V } object Test { - val x : (Base { type V = T[this.type] })#V = null + val x: (Base { type V = T[this.type] })#V = ??? val y = new x.U { } } diff --git a/tests/pos/t151.scala b/tests/pos/t151.scala index 86667b49f709..86e4a1caf1b2 100644 --- a/tests/pos/t151.scala +++ b/tests/pos/t151.scala @@ -1,6 +1,6 @@ abstract class Foo { type T; def foo(a: T): Int = 0; - val foo: Foo = null; + val foo: Foo = ???; def a: foo.T = a; } diff --git a/tests/pos/t1560.scala b/tests/pos/t1560.scala index dd76392e6679..3be2baad601a 100644 --- a/tests/pos/t1560.scala +++ b/tests/pos/t1560.scala @@ -4,7 +4,7 @@ object Test extends App { def t: T } - def b: Option[C[_]] = null + def b: Option[C[_]] = ??? def c = b match { case Some(b) => b.t diff --git a/tests/pos/t2023.scala b/tests/pos/t2023.scala index 21c6fc96a621..5aa6f926b686 100644 --- a/tests/pos/t2023.scala +++ b/tests/pos/t2023.scala @@ -1,7 +1,7 @@ trait C[A] object C { - implicit def ipl[A](implicit from: A => Ordered[A]): C[A] = null + implicit def ipl[A](implicit from: A => Ordered[A]): C[A] = ??? } object P { diff --git a/tests/pos/t2261.scala b/tests/pos/t2261.scala index 06360d50010c..18983ded8aa6 100644 --- a/tests/pos/t2261.scala +++ b/tests/pos/t2261.scala @@ -1,7 +1,7 @@ class Bob[T] object Test { implicit def foo2bar[T](xs: List[T]): Bob[T] = new Bob[T] - var x: Bob[Int] = null + var x: Bob[Int] = ??? x = List(1,2,3) // the problem here was that somehow the type variable that was used to infer the type argument for List.apply // would accumulate several conflicting constraints diff --git a/tests/pos/t2712-6.scala b/tests/pos/t2712-6.scala index dbba60472cb7..fb32149827ca 100644 --- a/tests/pos/t2712-6.scala +++ b/tests/pos/t2712-6.scala @@ -8,5 +8,5 @@ object Tags { trait Disjunction def meh[M[_], A](ma: M[A]): M[A] = ma - meh(null: Int @@ Disjunction)//.asInstanceOf[Int @@ Disjunction]) + meh(??? : Int @@ Disjunction)//.asInstanceOf[Int @@ Disjunction]) } diff --git a/tests/pos/t3528.scala b/tests/pos/t3528.scala index ff49b3e9298b..91bc2dada86d 100644 --- a/tests/pos/t3528.scala +++ b/tests/pos/t3528.scala @@ -4,5 +4,5 @@ class A { // 3528 comments def f2 = List(Set(1,2,3), List(1,2,3)) // 2322 - def f3 = List(null: Range, null: List[Int]) + def f3 = List(??? : Range, ??? : List[Int]) } diff --git a/tests/pos/t359.scala b/tests/pos/t359.scala index 11233c3ba458..e2bfc617ce56 100644 --- a/tests/pos/t359.scala +++ b/tests/pos/t359.scala @@ -6,8 +6,8 @@ object Bug359 { if (false) { f1(xs) } else { - val a: C = null; - val b: C = null; + val a: C = ???; + val b: C = ???; if (xs.isEmpty) a else b } } @@ -16,8 +16,8 @@ object Bug359 { g { xs => if (false) { - val a: C = null; - val b: C = null; + val a: C = ???; + val b: C = ???; if (xs.isEmpty) a else b } else { f2(xs); diff --git a/tests/pos/t443.scala b/tests/pos/t443.scala index f1f7ec258642..ea4668af47e3 100644 --- a/tests/pos/t443.scala +++ b/tests/pos/t443.scala @@ -1,13 +1,13 @@ object Test { def lookup(): Option[Tuple2[String, String]] = - ((null: Option[Tuple2[String, String]]) : @unchecked) match { + ((??? : Option[Tuple2[String, String]]) : @unchecked) match { case Some((_, _)) => if (true) - Some((null, null)) + Some((???, ???)) else lookup() match { - case Some(_) => Some(null) + case Some(_) => Some(???) case None => None } } diff --git a/tests/pos/t4579.scala b/tests/pos/t4579.scala index 500ffae40200..2ab202436b98 100644 --- a/tests/pos/t4579.scala +++ b/tests/pos/t4579.scala @@ -97,7 +97,7 @@ object LispCaseClasses extends Lisp { x6: Data, x7: Data, x8: Data, x9: Data): Data = CONS(x0, list(x1, x2, x3, x4, x5, x6, x7, x8, x9)); - var curexp: Data = null + var curexp: Data = ??? var trace: Boolean = false var indent: Int = 0 diff --git a/tests/pos/t5240.scala b/tests/pos/t5240.scala index 065d175f2f80..2af11490ebd9 100644 --- a/tests/pos/t5240.scala +++ b/tests/pos/t5240.scala @@ -6,6 +6,6 @@ package object foo { - var labels: Array[_ <: String] = null + var labels: Array[_ <: String] = ??? } diff --git a/tests/pos/t5359.scala b/tests/pos/t5359.scala index c22b2b1c768c..d319fff31519 100644 --- a/tests/pos/t5359.scala +++ b/tests/pos/t5359.scala @@ -11,7 +11,7 @@ object test { case class S1[F[_]]() extends Step[F] // okay - (null: Step[Option]) match { + (??? : Step[Option]) match { case S1() => } } diff --git a/tests/pos/t5399.scala b/tests/pos/t5399.scala index 0e7cce3c1776..49c3e60daa1b 100644 --- a/tests/pos/t5399.scala +++ b/tests/pos/t5399.scala @@ -18,8 +18,8 @@ class Foo { case class ScopedKey1[T](val foo: Init[T]) extends ScopedKey[T] - val scalaHome: Setting[Option[String]] = null - val scalaVersion: Setting[String] = null + val scalaHome: Setting[Option[String]] = ??? + val scalaVersion: Setting[String] = ??? def testPatternMatch(s: Setting[_]): Unit = { s.key match { diff --git a/tests/pos/t5399a.scala b/tests/pos/t5399a.scala index c40cef4f961b..7939c4d1f64e 100644 --- a/tests/pos/t5399a.scala +++ b/tests/pos/t5399a.scala @@ -8,8 +8,8 @@ class Foo { case class ScopedKey1[T](val foo: Init[T]) extends ScopedKey[T] - val scalaHome: Setting[Option[String]] = null - val scalaVersion: Setting[String] = null + val scalaHome: Setting[Option[String]] = ??? + val scalaVersion: Setting[String] = ??? def testPatternMatch(s: Setting[_]): Unit = { s.key match { diff --git a/tests/pos/t5683.scala b/tests/pos/t5683.scala index 05ab03579274..2c29af5e190e 100644 --- a/tests/pos/t5683.scala +++ b/tests/pos/t5683.scala @@ -6,10 +6,10 @@ object Test { def k[M[_], B](f: Int => M[B]): K[M, Int, B] = null - val okay1: K[StringW,Int,Int] = k{ (y: Int) => null: StringW[Int] } - val okay2 = k[StringW,Int]{ (y: Int) => null: W[String, Int] } + val okay1: K[StringW,Int,Int] = k{ (y: Int) => ??? : StringW[Int] } + val okay2 = k[StringW,Int]{ (y: Int) => ??? : W[String, Int] } - val crash: K[StringW,Int,Int] = k{ (y: Int) => null: W[String, Int] } + val crash: K[StringW,Int,Int] = k{ (y: Int) => ??? : W[String, Int] } // remove `extends NT[Int]`, and the last line gives an inference error // rather than a crash. @@ -18,6 +18,6 @@ object Test { // argument expression's type is not compatible with formal parameter type; // found : Int => Test.W[String,Int] // required: Int => ?M[?B] - // val crash: K[StringW,Int,Int] = k{ (y: Int) => null: W[String, Int] } + // val crash: K[StringW,Int,Int] = k{ (y: Int) => ??? : W[String, Int] } // ^ } diff --git a/tests/pos/t5720-ownerous.scala b/tests/pos/t5720-ownerous.scala index e171ce9c2a7e..b4251e72ab51 100644 --- a/tests/pos/t5720-ownerous.scala +++ b/tests/pos/t5720-ownerous.scala @@ -31,7 +31,7 @@ class C { def model = Option(m).getOrElse(M("bar")()).copy("baz")("empty") // style points for this version - def modish = ((null: Option[M]) getOrElse new M()()).copy()("empty") + def modish = ((??? : Option[M]) getOrElse new M()()).copy()("empty") // various simplifications are too simple case class N(currentUser: String = "anon") diff --git a/tests/pos/t5829.scala b/tests/pos/t5829.scala index 84b450ab31f7..d8489150358f 100644 --- a/tests/pos/t5829.scala +++ b/tests/pos/t5829.scala @@ -10,7 +10,7 @@ trait Universe { } object Test extends App { - val universe: Universe = null + val universe: Universe = ??? import universe._ def select: Select = ??? def ident: Ident = ??? diff --git a/tests/pos/t7369.scala b/tests/pos/t7369.scala index 2b43bcaabfe3..feb5cc81cc5b 100644 --- a/tests/pos/t7369.scala +++ b/tests/pos/t7369.scala @@ -1,6 +1,6 @@ object Test { val X, Y = true - (null: Tuple1[Boolean]) match { + (??? : Tuple1[Boolean]) match { case Tuple1(X) => case Tuple1(Y) => // unreachable case _ => @@ -17,7 +17,7 @@ object Test2 { val X: B = True val Y: B = False - (null: Tuple1[B]) match { + (??? : Tuple1[B]) match { case Tuple1(X) => case Tuple1(Y) => // no warning case _ => diff --git a/tests/pos/t7694.scala b/tests/pos/t7694.scala index 9852d5ec79d8..bcaf86a24e65 100644 --- a/tests/pos/t7694.scala +++ b/tests/pos/t7694.scala @@ -9,9 +9,9 @@ object Lub { // use named args transforms to include TypeTree() in the AST before refchecks. def foo(a: L[_, _], b: Any) = 0 - foo(b = 0, a = if (true) (null: L[A, A]) else (null: L[B, B])) + foo(b = 0, a = if (true) (??? : L[A, A]) else (??? : L[B, B])) - (if (true) (null: L[A, A]) else (null: L[B, B])).bar(b = 0, a = 0) + (if (true) (??? : L[A, A]) else (??? : L[B, B])).bar(b = 0, a = 0) } /* diff --git a/tests/pos/t7983.scala b/tests/pos/t7983.scala index fbeb7d3c59a8..b8ac20d2ff1b 100644 --- a/tests/pos/t7983.scala +++ b/tests/pos/t7983.scala @@ -25,7 +25,7 @@ class DivergenceTest { def map1[F, T](f: F)(implicit shape: Shape2[_ <: Flat, F, T]) = ??? - map1(((1, null: Coffees), 1)) - map1(((null: Coffees, 1), 1)) // fails with implicit divergence error in 2.11.0-M6, works under 2.10.3 + map1(((1, ??? : Coffees), 1)) + map1(((??? : Coffees, 1), 1)) // fails with implicit divergence error in 2.11.0-M6, works under 2.10.3 } } diff --git a/tests/pos/t8177d.scala b/tests/pos/t8177d.scala index d15a05a359c9..13c1d42b59c2 100644 --- a/tests/pos/t8177d.scala +++ b/tests/pos/t8177d.scala @@ -6,7 +6,7 @@ trait View[AIn] { } object Test { - val view: View[Int] = null + val view: View[Int] = ??? view f2 5 // fails } diff --git a/tests/pos/test4.scala b/tests/pos/test4.scala index 4fe65a8f1973..e0161955cf42 100644 --- a/tests/pos/test4.scala +++ b/tests/pos/test4.scala @@ -20,14 +20,14 @@ class O[X]() { trait I[Y] { def foo(x: X, y: Y): E = e; } - val i:I[E] = null; - val j:I[X] = null; + val i: I[E] = ???; + val j: I[X] = ???; } object ooo extends O[C]() { def main = { - val s: S = null; + val s: S = ???; import s._; foo(c,d); ooo.i.foo(c,e); @@ -37,7 +37,7 @@ object ooo extends O[C]() { } class Main() { - val s: S = null; + val s: S = ???; import s._; foo(c,d); ooo.i.foo(c,e); diff --git a/tests/pos/test4a.scala b/tests/pos/test4a.scala index 3efa059b73c2..33af3e121608 100644 --- a/tests/pos/test4a.scala +++ b/tests/pos/test4a.scala @@ -4,7 +4,7 @@ class O[X]() { trait I[Y] { def foo(y: Y): Y = y; } - val j:I[X] = null; + val j: I[X] = ???; } object p extends O[C]() { diff --git a/tests/pos/test4refine.scala b/tests/pos/test4refine.scala index 5e76d10be3a9..da179bae193c 100644 --- a/tests/pos/test4refine.scala +++ b/tests/pos/test4refine.scala @@ -21,15 +21,15 @@ abstract class O() { type Y; def foo(x: X, y: Y): E = e; } - val i:I { type Y = E } = null; - val j:I { type Y = X } = null; + val i: I { type Y = E } = ???; + val j: I { type Y = X } = ???; } object p extends O() { type X = C; def main = { - val s: S = null; + val s: S = ???; import s._; foo(c,d); p.i.foo(c,e); @@ -39,7 +39,7 @@ object p extends O() { } class Main() { - val s: S = null; + val s: S = ???; import s._; foo(c,d); p.i.foo(c,e); diff --git a/tests/pos/test5.scala b/tests/pos/test5.scala index c1947804879c..5906fc89c0fb 100644 --- a/tests/pos/test5.scala +++ b/tests/pos/test5.scala @@ -20,7 +20,7 @@ object test { def val_ix: X = val_ix; } - val i:I[G[P]] = null; + val i: I[G[P]] = ???; // Values with types P and i.X as seen from instances of M def val_mp: P = val_mp; @@ -28,7 +28,7 @@ object test { } class N[Q]() extends M[F[Q]]() { - val j:J[G[Q]] = null; + val j: J[G[Q]] = ???; abstract class J[Y]() extends I[G[Y]]() { // Values with types Y and X as seen from instances of J diff --git a/tests/pos/test5refine.scala b/tests/pos/test5refine.scala index 09ea179da9f7..42bd44262f2e 100644 --- a/tests/pos/test5refine.scala +++ b/tests/pos/test5refine.scala @@ -23,7 +23,7 @@ object test { def val_ix: X = val_ix; } - val i: I { type X = G { type Ig = P } } = null; + val i: I { type X = G { type Ig = P } } = ???; // Values with types P and i.X as seen from instances of M def val_mp: P = val_mp; @@ -33,7 +33,7 @@ object test { abstract class N() extends M() { type Q; type P = F { type If = Q }; - val j:J { type Y = G { type Ig = Q } } = null; + val j: J { type Y = G { type Ig = Q } } = ???; abstract class J() extends I() { type Y; diff --git a/tests/pos/testcast.scala b/tests/pos/testcast.scala index d4184e4f0b45..779f6b0a9593 100644 --- a/tests/pos/testcast.scala +++ b/tests/pos/testcast.scala @@ -7,7 +7,7 @@ class B extends A { } object B { - def view(x: B): B1 = null + def view(x: B): B1 = ??? } class B1 { @@ -15,13 +15,13 @@ class B1 { } object C { - implicit def view(x: A): B1 = null + implicit def view(x: A): B1 = ??? } object Test { import C.view - val b: B = null + val b: B = ??? println(b.bar) } diff --git a/tests/pos/virtpatmat_exist2.scala b/tests/pos/virtpatmat_exist2.scala index f6ebb3ee2f84..7b7a3c9f5bc0 100644 --- a/tests/pos/virtpatmat_exist2.scala +++ b/tests/pos/virtpatmat_exist2.scala @@ -2,14 +2,14 @@ class ParseResult[+T] case class MemoEntry[+T](var r: Either[Nothing,ParseResult[_]]) object Test { - def grow[T]: ParseResult[T] = (null: MemoEntry[T]) match { + def grow[T]: ParseResult[T] = (??? : MemoEntry[T]) match { case MemoEntry(Right(x: ParseResult[_])) => x.asInstanceOf[ParseResult[T]] } // what's the _$1 doing there? // def grow[T >: Nothing <: Any]: ParseResult[T] = { // import OptionMatching._ - // runOrElse[MemoEntry[T], ParseResult[T]]((null: MemoEntry[T]))(((x1: MemoEntry[T]) => + // runOrElse[MemoEntry[T], ParseResult[T]]((??? : MemoEntry[T]))(((x1: MemoEntry[T]) => // (MemoEntry.unapply[T](x1).flatMap[ParseResult[T]](((x4: Either[Nothing,ParseResult[_]]) => // guard[Right[Nothing,ParseResult[_]]](x4.isInstanceOf[Right[Nothing,ParseResult[_]]], x4.asInstanceOf[Right[Nothing,ParseResult[_]]]).flatMap[ParseResult[T]](((cp3: Right[Nothing,ParseResult[_]]) => // scala.Right.unapply[Nothing, ParseResult[_]](cp3).flatMap[ParseResult[T]](((x5: ParseResult[_]) => diff --git a/tests/pos/virtpatmat_exist4.scala b/tests/pos/virtpatmat_exist4.scala index 728006276350..1e9972ce953e 100644 --- a/tests/pos/virtpatmat_exist4.scala +++ b/tests/pos/virtpatmat_exist4.scala @@ -17,7 +17,7 @@ trait MemberHandlers { } object Test { - var intp: IMain with MemberHandlers = null + var intp: IMain with MemberHandlers = ??? val handlers = intp.handlers handlers.filterNot(_.importedSymbols.isEmpty).zipWithIndex foreach { From b5afd26ac62907b9338380caf31ac6bfc942f2f1 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 21 Aug 2018 13:21:27 -0400 Subject: [PATCH 003/127] Change typing of "throw" expressions to allow a nullable argument The JLS (https://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.18) explains that "throw" should accept a null argument (in which case it throws an NPE): ``` If evaluation of the Expression completes normally, producing a null value, then an instance V' of class NullPointerException is created and thrown instead of null. The throw statement then completes abruptly, the reason being a throw with value V'. ``` So changed the typer so the prototype allows for null. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/pos/throw-null.scala | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/pos/throw-null.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 833e25a55976..07a55a9288cf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1178,7 +1178,7 @@ class Typer extends Namer } def typedThrow(tree: untpd.Throw)(implicit ctx: Context): Tree = track("typedThrow") { - val expr1 = typed(tree.expr, defn.ThrowableType) + val expr1 = typed(tree.expr, OrType(defn.ThrowableType, defn.NullType)) Throw(expr1).withPos(tree.pos) } diff --git a/tests/pos/throw-null.scala b/tests/pos/throw-null.scala new file mode 100644 index 000000000000..df6aeb6315ea --- /dev/null +++ b/tests/pos/throw-null.scala @@ -0,0 +1,5 @@ + +object Foo { + val bar: NullPointerException | Null = ??? + throw bar +} From 3eb38747293f6cef157738be47a3b8f2fe0c9dc0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 21 Aug 2018 16:06:34 -0400 Subject: [PATCH 004/127] Revert improperly modified test --- tests/pos/t4579.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/t4579.scala b/tests/pos/t4579.scala index 2ab202436b98..500ffae40200 100644 --- a/tests/pos/t4579.scala +++ b/tests/pos/t4579.scala @@ -97,7 +97,7 @@ object LispCaseClasses extends Lisp { x6: Data, x7: Data, x8: Data, x9: Data): Data = CONS(x0, list(x1, x2, x3, x4, x5, x6, x7, x8, x9)); - var curexp: Data = ??? + var curexp: Data = null var trace: Boolean = false var indent: Int = 0 From fbfbd4f4801a667bb98f4fded890a0869d6bb23c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 22 Aug 2018 11:02:59 -0400 Subject: [PATCH 005/127] Addititional automated conversion of null to ??? This time we convert defs as well: def foo(x: String): String = null => def foo(x: String): String = ??? --- tests/pos-scala2/i3396.scala | 6 +++--- tests/pos-scala2/t3568.scala | 2 +- tests/pos/Transactions.scala | 2 +- tests/pos/existentials-harmful.scala | 2 +- tests/pos/hk.scala | 2 +- tests/pos/i576.scala | 4 ++-- tests/pos/infer.scala | 2 +- tests/pos/return_thistype.scala | 2 +- tests/pos/sams.scala | 4 ++-- tests/pos/t0905.scala | 2 +- tests/pos/t1272.scala | 4 ++-- tests/pos/t1832.scala | 2 +- tests/pos/t245.scala | 8 ++++---- tests/pos/t247.scala | 4 ++-- tests/pos/t2741/2741_1.scala | 2 +- tests/pos/t359.scala | 2 +- tests/pos/t3866.scala | 4 ++-- tests/pos/t4869.scala | 2 +- tests/pos/t5012.scala | 2 +- tests/pos/t5683.scala | 2 +- tests/pos/t5727.scala | 6 +++--- tests/pos/t6942/t6942.scala | 2 +- tests/pos/t6966.scala | 10 +++++----- tests/pos/t7944.scala | 2 +- tests/pos/t807.scala | 2 +- tests/pos/t8230a.scala | 8 ++++---- tests/pos/wildcardBoundInference.scala | 6 +++--- tests/pos/z1730.scala | 2 +- 28 files changed, 49 insertions(+), 49 deletions(-) diff --git a/tests/pos-scala2/i3396.scala b/tests/pos-scala2/i3396.scala index 2091f6547976..cdf6cbc38223 100644 --- a/tests/pos-scala2/i3396.scala +++ b/tests/pos-scala2/i3396.scala @@ -5,11 +5,11 @@ object Test { // Negation Tagged: NotTagged[A] is available only if there are no Tagged[A] in scope. trait NotTagged[A] trait NotTaggedLowPrio { - implicit def notTaggedInstance[A]: NotTagged[A] = null + implicit def notTaggedInstance[A]: NotTagged[A] = ??? } object NotTagged extends NotTaggedLowPrio { - implicit def notTaggedAmbiguity1[A](implicit ev: Tagged[A]): NotTagged[A] = null - implicit def notTaggedAmbiguity2[A](implicit ev: Tagged[A]): NotTagged[A] = null + implicit def notTaggedAmbiguity1[A](implicit ev: Tagged[A]): NotTagged[A] = ??? + implicit def notTaggedAmbiguity2[A](implicit ev: Tagged[A]): NotTagged[A] = ??? } diff --git a/tests/pos-scala2/t3568.scala b/tests/pos-scala2/t3568.scala index 50f0cdb2ebf3..76be56f8b13d 100644 --- a/tests/pos-scala2/t3568.scala +++ b/tests/pos-scala2/t3568.scala @@ -41,6 +41,6 @@ package buffer { class ArrayVec2(val backingSeq: ArrayFloat1) extends GenericSeq[Vec2] with DataArray[Vec2] { def this() = this(new ArrayFloat1) - def apply(i: Int) :Vec2 = null + def apply(i: Int) : Vec2 = ??? } } diff --git a/tests/pos/Transactions.scala b/tests/pos/Transactions.scala index eee146c813eb..1c483a666cfd 100644 --- a/tests/pos/Transactions.scala +++ b/tests/pos/Transactions.scala @@ -62,7 +62,7 @@ trait Transactional { var readers: Transaction var writer: Transaction - def currentWriter(): Transaction = null + def currentWriter(): Transaction = ??? if (writer == null) null else if (writer.status == Transaction.Running) writer else { diff --git a/tests/pos/existentials-harmful.scala b/tests/pos/existentials-harmful.scala index 91dbd4dfda34..aa0e43096dc7 100644 --- a/tests/pos/existentials-harmful.scala +++ b/tests/pos/existentials-harmful.scala @@ -9,7 +9,7 @@ object ExistentialsConsideredHarmful { trait Tools[A] { def shave(a: A): A } - def tools[A](a: A): Tools[A] = null // dummy + def tools[A](a: A): Tools[A] = ??? // dummy case class TransportBox[A <: Animal](animal: A, tools: Tools[A]) { def label: String = animal.name diff --git a/tests/pos/hk.scala b/tests/pos/hk.scala index a8f2aa5971cc..256bc580c2ae 100644 --- a/tests/pos/hk.scala +++ b/tests/pos/hk.scala @@ -23,7 +23,7 @@ object hk0 { object higherKinded { - type Untyped = Null + type Untyped = Nothing class Tree[-T >: Untyped] { type ThisType[-U >: Untyped] <: Tree[U] diff --git a/tests/pos/i576.scala b/tests/pos/i576.scala index 77b38d742d65..6a091f4b19e7 100644 --- a/tests/pos/i576.scala +++ b/tests/pos/i576.scala @@ -1,7 +1,7 @@ class A object Impl { - def foo()(implicit x: A = null): Int = 2 + def foo()(implicit x: A = ???): Int = 2 def test: Int = { foo()() // ok foo() // did not work before, does now @@ -10,7 +10,7 @@ object Impl { // same with multiple parameters object Impl2 { - def foo()(implicit ev: Int, x: A = null): Int = 2 + def foo()(implicit ev: Int, x: A = ???): Int = 2 def test: Int = { implicit val ii: Int = 1 foo() diff --git a/tests/pos/infer.scala b/tests/pos/infer.scala index 6aeed4049145..4715fda41387 100644 --- a/tests/pos/infer.scala +++ b/tests/pos/infer.scala @@ -5,7 +5,7 @@ object test { case class Cons[a, b <: a](x: a, xs: List[b]) extends List[a] case object Nil extends List[Nothing] def nil[n]: List[n] = Nil - def cons[a](x: a, xs: List[a]): List[a] = null + def cons[a](x: a, xs: List[a]): List[a] = ??? val x: List[Int] = Nil.::(1) val y: List[Int] = nil.::(1) } diff --git a/tests/pos/return_thistype.scala b/tests/pos/return_thistype.scala index c0736c0ad985..df7a070f6c13 100644 --- a/tests/pos/return_thistype.scala +++ b/tests/pos/return_thistype.scala @@ -3,6 +3,6 @@ class As { class A { def foo: A.this.type = bar.asInstanceOf[A.this.type] def foo2: this.type = bar.asInstanceOf[this.type] - def bar: A = null + def bar: A = ??? } } diff --git a/tests/pos/sams.scala b/tests/pos/sams.scala index d150e73d5eaa..cdfa01dc2deb 100644 --- a/tests/pos/sams.scala +++ b/tests/pos/sams.scala @@ -77,8 +77,8 @@ class T { object SI9943 { class Foo[T] { - def toMap[K, V](implicit ev: Foo[T] <:< Foo[(K, V)]): Foo[Map[K, V]] = null - def toMap[K](keySelector: T => K): Foo[Map[K, T]] = null + def toMap[K, V](implicit ev: Foo[T] <:< Foo[(K, V)]): Foo[Map[K, V]] = ??? + def toMap[K](keySelector: T => K): Foo[Map[K, T]] = ??? } object Foo { diff --git a/tests/pos/t0905.scala b/tests/pos/t0905.scala index fff6aee8a41f..ec128e4dc94f 100644 --- a/tests/pos/t0905.scala +++ b/tests/pos/t0905.scala @@ -1,6 +1,6 @@ object Test { trait A[T] - def f(implicit p: A[_]) = null + def f(implicit p: A[_]) = ??? implicit val x: A[_] = ??? println(f) } diff --git a/tests/pos/t1272.scala b/tests/pos/t1272.scala index 916b783bbb3e..831e6807fbb5 100644 --- a/tests/pos/t1272.scala +++ b/tests/pos/t1272.scala @@ -1,7 +1,7 @@ object ImplicitTest { implicit val i : Int = 10 - implicit def a(implicit i : Int) : Array[Byte] = null - implicit def b[T](implicit i : Int) : Array[T] = null + implicit def a(implicit i : Int) : Array[Byte] = ??? + implicit def b[T](implicit i : Int) : Array[T] = ??? def fn[T](implicit x : T) = 0 diff --git a/tests/pos/t1832.scala b/tests/pos/t1832.scala index c34fe4bfa067..f5b5975a7db1 100644 --- a/tests/pos/t1832.scala +++ b/tests/pos/t1832.scala @@ -4,7 +4,7 @@ trait Cloning { abstract class Star { def *(a: Cloning.this.Foo): Cloning.this.Foo } - implicit def mkStar(i: Int): Star = new Star { def *(a: Foo): Foo = null } + implicit def mkStar(i: Int): Star = new Star { def *(a: Foo): Foo = ??? } val pool = 4 * fn { case ghostSYMBOL: Int => ghostSYMBOL * 2 } } diff --git a/tests/pos/t245.scala b/tests/pos/t245.scala index 570ac4178d29..f947e4b6a39a 100644 --- a/tests/pos/t245.scala +++ b/tests/pos/t245.scala @@ -6,11 +6,11 @@ object Test { def foo(i: Int): Int = 0 - def fun0 : Value = null - def fun0(i: Int ): Value = null + def fun0 : Value = ??? + def fun0(i: Int ): Value = ??? - def fun1(i: Int ): Value = null - def fun1(l: Long): Value = null + def fun1(i: Int ): Value = ??? + def fun1(l: Long): Value = ??? foo(fun0 ); foo(fun1(new Value)); diff --git a/tests/pos/t247.scala b/tests/pos/t247.scala index fdcafeb2c6cc..71c38e5e6afd 100644 --- a/tests/pos/t247.scala +++ b/tests/pos/t247.scala @@ -20,7 +20,7 @@ class TreeMap[KEY,VALUE](_factory:TreeMapFactory[KEY]) extends Tree[KEY,Tuple2[K val factory = _factory val order = _factory.order; def this(newOrder:Order[KEY]) = this(new TreeMapFactory[KEY](newOrder)); - def get(key:KEY) = null; - def iterator:Iterator[Tuple2[KEY,VALUE]] = null; + def get(key: KEY) = ???; + def iterator: Iterator[Tuple2[KEY,VALUE]] = ???; override def size = super[Tree].size } diff --git a/tests/pos/t2741/2741_1.scala b/tests/pos/t2741/2741_1.scala index d9d04f7ab0b9..9b8a2702bf51 100644 --- a/tests/pos/t2741/2741_1.scala +++ b/tests/pos/t2741/2741_1.scala @@ -3,7 +3,7 @@ trait Partial { } trait MA[M[_]] trait MAs { - val a: MA[Partial#Apply] = null // after compilation, the type is pickled as `MA[ [B] List[B] ]` + val a: MA[Partial#Apply] = ??? // after compilation, the type is pickled as `MA[ [B] List[B] ]` } object Scalaz extends MAs diff --git a/tests/pos/t359.scala b/tests/pos/t359.scala index e2bfc617ce56..544a97aa418f 100644 --- a/tests/pos/t359.scala +++ b/tests/pos/t359.scala @@ -24,5 +24,5 @@ object Bug359 { } } } - private def g(op: List[C] => C): C = null; + private def g(op: List[C] => C): C = ???; } diff --git a/tests/pos/t3866.scala b/tests/pos/t3866.scala index f1f64edb9597..ec8d2f7f8ce8 100644 --- a/tests/pos/t3866.scala +++ b/tests/pos/t3866.scala @@ -6,8 +6,8 @@ abstract class ImplicitRepeated { def f[N, R <: List[_]](props: String, elems: T[N, R]*): Unit // alternative b) // the following implicit causes "cannot be applied" errors - implicit def xToRight(r: X): T[Nothing, X] = null - implicit def anyToN[N](x: N): T[N, Nothing] = null + implicit def xToRight(r: X): T[Nothing, X] = ??? + implicit def anyToN[N](x: N): T[N, Nothing] = ??? f("A", 1, 2) // should be implicitly resolved to alternative b) diff --git a/tests/pos/t4869.scala b/tests/pos/t4869.scala index f84aa4ed079e..a3900465dac1 100644 --- a/tests/pos/t4869.scala +++ b/tests/pos/t4869.scala @@ -3,6 +3,6 @@ class C[T] class A { - def f[T](x: T): C[_ <: T] = null + def f[T](x: T): C[_ <: T] = ??? def g = List(1d) map f } diff --git a/tests/pos/t5012.scala b/tests/pos/t5012.scala index 84404495c456..2c960c8318cc 100644 --- a/tests/pos/t5012.scala +++ b/tests/pos/t5012.scala @@ -3,7 +3,7 @@ class D { } class C { - def m: D = { + def m: D | Null = { if ("abc".length == 0) { object p // (program point 2) } diff --git a/tests/pos/t5683.scala b/tests/pos/t5683.scala index 2c29af5e190e..7113d4b9600a 100644 --- a/tests/pos/t5683.scala +++ b/tests/pos/t5683.scala @@ -4,7 +4,7 @@ object Test { type StringW[T] = W[String, T] trait K[M[_], A, B] - def k[M[_], B](f: Int => M[B]): K[M, Int, B] = null + def k[M[_], B](f: Int => M[B]): K[M, Int, B] = ??? val okay1: K[StringW,Int,Int] = k{ (y: Int) => ??? : StringW[Int] } val okay2 = k[StringW,Int]{ (y: Int) => ??? : W[String, Int] } diff --git a/tests/pos/t5727.scala b/tests/pos/t5727.scala index 2c6c0f3056a9..673b05d1a010 100644 --- a/tests/pos/t5727.scala +++ b/tests/pos/t5727.scala @@ -14,8 +14,8 @@ object Test { abstract class Base[+T] { def apply(f: String): Res[T] // 'i' crashes the compiler, similarly if we use currying - //def |[U >: T](a: => Base[U], i: SomeInfo = NoInfo): Base[U] = null - def bar[U >: T](a: => Base[U], i: SomeInfo = NoInfo): Base[U] = null + //def |[U >: T](a: => Base[U], i: SomeInfo = NoInfo): Base[U] = ??? + def bar[U >: T](a: => Base[U], i: SomeInfo = NoInfo): Base[U] = ??? } implicit def fromStringToBase(a: String): Base[String] = new Base[String] { def apply(in: String) = NotRes } @@ -24,7 +24,7 @@ object Test { //def Sample: Base[Any] = ( rep("foo" | "bar") | "sth") def Sample: Base[Any] = ( rep("foo" bar "bar") bar "sth") - def rep[T](p: => Base[T]): Base[T] = null // whatever + def rep[T](p: => Base[T]): Base[T] = ??? // whatever def main(args: Array[String]): Unit = { } diff --git a/tests/pos/t6942/t6942.scala b/tests/pos/t6942/t6942.scala index 77963d263487..8d729122fe04 100644 --- a/tests/pos/t6942/t6942.scala +++ b/tests/pos/t6942/t6942.scala @@ -2,7 +2,7 @@ // its budget should suffice for these simple matches (they do have a large search space) class Test { import foo.Bar // a large enum - def exhaustUnreachabilitysStack_ENUM_STYLE = (null: Bar) match { + def exhaustUnreachabilitysStack_ENUM_STYLE = (???: Bar) match { case Bar.BULGARIA => case _ => } diff --git a/tests/pos/t6966.scala b/tests/pos/t6966.scala index cd91221a651a..ba9e0f000d00 100644 --- a/tests/pos/t6966.scala +++ b/tests/pos/t6966.scala @@ -2,12 +2,12 @@ import Ordering.{Byte, comparatorToOrdering} trait Format[T] trait InputCache[T] object CacheIvy { - implicit def basicInputCache[I](implicit fmt: Format[I], eqv: Equiv[I]): InputCache[I] = null - implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] = null - implicit def hNilCache: InputCache[HNil] = null - implicit def ByteArrayFormat: Format[Array[Byte]] = null + implicit def basicInputCache[I](implicit fmt: Format[I], eqv: Equiv[I]): InputCache[I] = ??? + implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] = ??? + implicit def hNilCache: InputCache[HNil] = ??? + implicit def ByteArrayFormat: Format[Array[Byte]] = ??? type :+:[H, T <: HList] = HCons[H,T] - implicit def hConsCache[H, T <: HList](implicit head: InputCache[H], tail: InputCache[T]): InputCache[H :+: T] = null + implicit def hConsCache[H, T <: HList](implicit head: InputCache[H], tail: InputCache[T]): InputCache[H :+: T] = ??? hConsCache[Array[Byte], HNil] } diff --git a/tests/pos/t7944.scala b/tests/pos/t7944.scala index 2fe2c5866dbf..fe364cfdee0e 100644 --- a/tests/pos/t7944.scala +++ b/tests/pos/t7944.scala @@ -2,7 +2,7 @@ class M[+A, +B] object Test { implicit class EitherOps[A, B](self: Either[A, B]) { - def disjunction: M[A, B] = null + def disjunction: M[A, B] = ??? } def foo = { diff --git a/tests/pos/t807.scala b/tests/pos/t807.scala index 0eeb92ea2476..79d8177a02b3 100644 --- a/tests/pos/t807.scala +++ b/tests/pos/t807.scala @@ -4,7 +4,7 @@ trait Matcher { type Match <: Link { type Match = Link.this.Self; } } trait HasLinks { - def link(b : Boolean) : Link = null; + def link(b : Boolean) : Link = ???; } } diff --git a/tests/pos/t8230a.scala b/tests/pos/t8230a.scala index dfbae51eeee2..8d36f4a1fd7f 100644 --- a/tests/pos/t8230a.scala +++ b/tests/pos/t8230a.scala @@ -1,12 +1,12 @@ trait Arr[T] object Arr { - def apply[T](xs: T): Arr[T] = null - def apply(x: Long) : Arr[Long] = null + def apply[T](xs: T): Arr[T] = ??? + def apply(x: Long) : Arr[Long] = ??? } object I { - implicit def arrToTrav[T] (a: Arr[T]) : Traversable[T] = null - implicit def longArrToTrav(a: Arr[Long]): Traversable[Long] = null + implicit def arrToTrav[T] (a: Arr[T]) : Traversable[T] = ??? + implicit def longArrToTrav(a: Arr[Long]): Traversable[Long] = ??? } object Test { diff --git a/tests/pos/wildcardBoundInference.scala b/tests/pos/wildcardBoundInference.scala index 65553ed93619..207acae81705 100644 --- a/tests/pos/wildcardBoundInference.scala +++ b/tests/pos/wildcardBoundInference.scala @@ -7,7 +7,7 @@ public class Exist { } */ class Exist[T <: String] { - def foo: Exist[_] = null + def foo: Exist[_] = ??? } /* @@ -18,7 +18,7 @@ public class ExistF> { */ class ExistF[T <: ExistF[T]] { - def foo: ExistF[_] = null + def foo: ExistF[_] = ??? } /* @@ -29,7 +29,7 @@ public class ExistIndir { */ class ExistIndir[T <: String, U <: T] { - def foo: ExistIndir[_, _] = null + def foo: ExistIndir[_, _] = ??? } class Test { diff --git a/tests/pos/z1730.scala b/tests/pos/z1730.scala index 574b9bbd03d3..896e2942180d 100644 --- a/tests/pos/z1730.scala +++ b/tests/pos/z1730.scala @@ -6,7 +6,7 @@ class X[R] { } class Boo { - implicit def toX[R](v: R) : X[R] = null + implicit def toX[R](v: R) : X[R] = ??? def goo2: Unit = { 3.xx(34) } From 8ebd3eb26994f6aabfabde1da5329baaf5e94ef7 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 22 Aug 2018 11:13:45 -0400 Subject: [PATCH 006/127] Fix additional positive tests --- tests/pos/t1107a.scala | 3 ++- tests/pos/t2613.scala | 2 +- tests/pos/t2913.scala | 2 +- tests/pos/unapplyNeedsMemberType.scala | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/pos/t1107a.scala b/tests/pos/t1107a.scala index 0bf40bb4cc6e..0e433a97a667 100644 --- a/tests/pos/t1107a.scala +++ b/tests/pos/t1107a.scala @@ -1,6 +1,7 @@ object F { + implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] type AnyClass = Class[_] - def tryf[T](ignore: List[AnyClass])(f: => T): Any = { + def tryf[T](ignore: List[AnyClass] | Null)(f: => T): Any = { try { f } catch { diff --git a/tests/pos/t2613.scala b/tests/pos/t2613.scala index 17ebe2d7e9bf..d5b7d7da86a2 100644 --- a/tests/pos/t2613.scala +++ b/tests/pos/t2613.scala @@ -7,5 +7,5 @@ object Test { type M = MyRelation[_ <: Row, _ <: MyRelation[_, _]] - val (x,y): (String, M) = null + val (x,y): (String, M) = ??? } diff --git a/tests/pos/t2913.scala b/tests/pos/t2913.scala index f91ed7b51318..ffb87b1257cc 100644 --- a/tests/pos/t2913.scala +++ b/tests/pos/t2913.scala @@ -47,7 +47,7 @@ object test1 { object Main { def main(args : Array[String]): Unit = { val fn = (a : Int, str : String) => "a: " + a + ", str: " + str - implicit def fx[T](f : (T,String) => String): T => String = (x:T) => f(x,null) + implicit def fx[T](f : (T,String) => String): T => String = (x:T) => f(x,???) println(fn(1)) () } diff --git a/tests/pos/unapplyNeedsMemberType.scala b/tests/pos/unapplyNeedsMemberType.scala index 3a96e189afc6..c36d5fd058e4 100644 --- a/tests/pos/unapplyNeedsMemberType.scala +++ b/tests/pos/unapplyNeedsMemberType.scala @@ -20,6 +20,6 @@ class Join[a] extends Gunk[a] { def unapply_Cons(s: Any) = s match { case App(Cons(x, xs), ys) => Some((x, append(xs, ys))) - case _ => null + case _ => None } } From 73ffa5d86982957a935888369bf28d409dc90492 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 24 Aug 2018 13:15:33 -0400 Subject: [PATCH 007/127] Revert incorrect change to backend interface Commit cc643747e3931a96058d61fa398e691fa3197617 incorrectly updated `isBottomClass` in the bakend interface. Revert the change because in the backend types are nullable. --- .../src/dotty/tools/backend/jvm/DottyBackendInterface.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala index 5e1454c65fe4..781885faec65 100644 --- a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala +++ b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala @@ -685,7 +685,7 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma ((sym is Flags.JavaStatic) || (owner is Flags.ImplClass) || toDenot(sym).hasAnnotation(ctx.definitions.ScalaStaticAnnot)) // guard against no sumbol cause this code is executed to select which call type(static\dynamic) to use to call array.clone - def isBottomClass: Boolean = sym ne defn.NothingClass + def isBottomClass: Boolean = (sym ne defn.NullClass) && (sym ne defn.NothingClass) def isBridge: Boolean = sym is Flags.Bridge def isArtifact: Boolean = sym is Flags.Artifact def hasEnumFlag: Boolean = sym is Flags.JavaEnum From edc63207286bb5522f678cacdb952fa2f10c3fcd Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 22 Aug 2018 18:04:28 -0400 Subject: [PATCH 008/127] Implement Java nullability transform This first version of the transform adds "|Null" to field types and method argument and return types of Java classes. e.g. class C { String foo(String x); } becomes class C { String|Null foo(String|Null x); } Type parameters also get nullified (e.g. "ArrayList[T] => ArrayList[T|Null]"). --- .../dotty/tools/dotc/core/Definitions.scala | 4 +-- .../src/dotty/tools/dotc/core/Flags.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 7 +++++ .../src/dotty/tools/dotc/core/Types.scala | 27 +++++++++++++++++ .../dotc/core/classfile/ClassfileParser.scala | 5 ++-- .../src/dotty/tools/dotc/typer/Namer.scala | 9 +++++- tests/neg/nullify.scala | 20 +++++++++++++ tests/pos-java-interop/nullify.scala | 29 +++++++++++++++++++ tests/pos/raw-map/S_2.scala | 2 +- tests/pos/t1896/D0.scala | 2 +- 10 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 tests/neg/nullify.scala create mode 100644 tests/pos-java-interop/nullify.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 7c574a70d369..6ceab97b9fe1 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -327,8 +327,8 @@ class Definitions { pt => MethodType(List(FunctionOf(Nil, pt.paramRefs(0))), pt.paramRefs(0))) /** Method representing a throw */ - lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw, - MethodType(List(ThrowableType), NothingType)) + lazy val throwMethod = enterMethod(OpsPackageClass, nme.THROWkw, + MethodType(List(OrType(ThrowableType, NullType)), NothingType)) lazy val NothingClass: ClassSymbol = enterCompleteClassSymbol( ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef)) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 26a71b41747b..4b2423fe313e 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -478,7 +478,7 @@ object Flags { final val FromStartFlags: FlagSet = Module | Package | Deferred | Method.toCommonFlags | HigherKinded.toCommonFlags | Param | ParamAccessor.toCommonFlags | - Scala2ExistentialCommon | MutableOrOpaque | Touched | JavaStatic | + Scala2ExistentialCommon | MutableOrOpaque | Touched | JavaStatic | JavaDefined | CovariantOrOuter | ContravariantOrLabel | CaseAccessor.toCommonFlags | Extension.toCommonFlags | NonMember | ImplicitCommon | Permanent | Synthetic | SuperAccessorOrScala2x | Inline diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f50d1a9a9896..7835d21ee337 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1603,6 +1603,13 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] { * an OrType, the lub will be computed using TypeCreator#erasedLub. */ final def orType(tp1: Type, tp2: Type, isErased: Boolean = ctx.erasedTypes): Type = { + if (isErased) { + // After erasure, C | Null is just C, if C is a reference type. + // This is because types are nullable for the JVM, no matter what + // our non-nullable type system says. + if (tp1.widenDealias == defn.NullType && tp2.derivesFrom(defn.ObjectClass)) return tp2 + if (tp2.widenDealias == defn.NullType && tp1.derivesFrom(defn.ObjectClass)) return tp1 + } val t1 = distributeOr(tp1, tp2) if (t1.exists) t1 else { diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c52b244d1671..e53c4758719d 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4726,6 +4726,33 @@ object Types { protected def reapply(tp: Type): Type = apply(tp) } + /** Adds "| Null" to the relevant places of a Java type to reflect the fact + * that Java types remain nullable by default. + * + * nullify(T) = T | Null if T is a type parameter or class or interface + * nullify(C[S]) = C[nullify(S)] | Null if C is a generic class + */ + class NullifyMap(implicit ctx: Context) extends TypeMap { + def shouldNullify(tp: TypeRef): Boolean = { + !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) + } + + override def apply(tp: Type): Type = { + tp match { + case tp: MethodType => + mapOver(tp) + case tp: TypeAlias => + mapOver(tp) + case tp: TypeRef if shouldNullify(tp) => + OrType(tp, defn.NullType) + case tp@RefinedType(parent, name, info) => + OrType(derivedRefinedType(tp, parent, this(info)), defn.NullType) + case _ => + tp + } + } + } + /** A range of possible types between lower bound `lo` and upper bound `hi`. * Only used internally in `ApproximatingTypeMap`. */ diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 80377807dedc..36bf3f7c0802 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -157,7 +157,7 @@ class ClassfileParser( enterOwnInnerClasses() - classRoot.setFlag(sflags) + classRoot.setFlag(Flags.JavaDefined | sflags) moduleRoot.setFlag(Flags.JavaDefined | Flags.ModuleClassCreationFlags) setPrivateWithin(classRoot, jflags) setPrivateWithin(moduleRoot, jflags) @@ -269,7 +269,8 @@ class ClassfileParser( setPrivateWithin(denot, jflags) denot.info = translateTempPoly(parseAttributes(sym, denot.info)) if (isConstructor) normalizeConstructorInfo() - + val nullMap = new NullifyMap + if (!isConstructor) denot.info = nullMap(denot.info) if ((denot is Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0) denot.info = arrayToRepeated(denot.info) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index fa881495dcf9..066325492871 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1176,7 +1176,14 @@ class Namer { typer: Typer => case _ => WildcardType } - paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + val resTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + val isConstructor = mdef.name == nme.CONSTRUCTOR + if (mdef.mods.is(JavaDefined) && !isConstructor) { + val nullMap = new NullifyMap + nullMap(resTpe) + } else { + resTpe + } } /** The type signature of a DefDef with given symbol */ diff --git a/tests/neg/nullify.scala b/tests/neg/nullify.scala new file mode 100644 index 000000000000..ad4bbee82d18 --- /dev/null +++ b/tests/neg/nullify.scala @@ -0,0 +1,20 @@ + +// Test that we mark fields and methods in Java classes as nullable. +class Foo { + + def s[T](x: T | Null): T = x.asInstanceOf[T] + + def foo = { + import java.util.ArrayList + val x = new ArrayList[String]() + x.add("Hello") // Allowed since `add` takes String | Null as argument + x.add(null) + val r: String = x.get(0) // error: got String | Null instead of String + val ll = new ArrayList[ArrayList[ArrayList[String]]] + val level1: ArrayList[ArrayList[String]] = ll.get(0) // error + val level2: ArrayList[String] = s(ll.get(0)).get(0) // error + val level3: String = s(s(ll.get(0)).get(0)).get(0) // error + val ok: String = s(s(s(ll.get(0)).get(0)).get(0)) + } + +} diff --git a/tests/pos-java-interop/nullify.scala b/tests/pos-java-interop/nullify.scala new file mode 100644 index 000000000000..cfd0bd630784 --- /dev/null +++ b/tests/pos-java-interop/nullify.scala @@ -0,0 +1,29 @@ + +// Tests that we add "| Null" to Java fields and methods. +class Foo { + + // So that the chained accessors compile. + implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] + + def foo = { + import java.util.ArrayList + import java.util.Iterator + val x = new ArrayList[String]() + x.add(null) // | Null added to a method argument + val y = x.get(0) + if (y == null) { // | Null added to return type + } + + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() // test nested nullable containers + if (x3.get(0) == null) { + } + if (x3.get(0).get(0) == null) { + } + if (x3.get(0).get(0).get(0) == null) { + } + + val it = x3.iterator() // it: Iterator[Sting] | Null + if (it == null) { + } + } +} diff --git a/tests/pos/raw-map/S_2.scala b/tests/pos/raw-map/S_2.scala index d2886fdce9e4..45c3ebe20f20 100644 --- a/tests/pos/raw-map/S_2.scala +++ b/tests/pos/raw-map/S_2.scala @@ -1,6 +1,6 @@ class Foo { def foo: Unit = { - val x: J_1 = null + val x: J_1 = ??? x.setRawType(new java.util.HashMap) } } diff --git a/tests/pos/t1896/D0.scala b/tests/pos/t1896/D0.scala index 6b3150d96916..cd72d1bfdc1d 100644 --- a/tests/pos/t1896/D0.scala +++ b/tests/pos/t1896/D0.scala @@ -7,5 +7,5 @@ trait A { } trait B extends A { - def f: Unit = { super.m(null) } + def f: Unit = { super.m(???) } } From e4ee72cd51eb1e3da2fe4b9d1454c8accb81c836 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 27 Aug 2018 15:53:32 -0400 Subject: [PATCH 009/127] Refactor tests and add new source test --- ...fy.scala => explicit-null-classfile.scala} | 2 +- ...fy.scala => explicit-null-classfile.scala} | 2 +- .../explicit-null-source/ArrayList.java | 6 ++++ .../explicit-null-source/Iterator.java | 3 ++ .../explicit-null-source/source.scala | 29 +++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) rename tests/neg/{nullify.scala => explicit-null-classfile.scala} (89%) rename tests/pos-java-interop/{nullify.scala => explicit-null-classfile.scala} (90%) create mode 100644 tests/pos-java-interop/explicit-null-source/ArrayList.java create mode 100644 tests/pos-java-interop/explicit-null-source/Iterator.java create mode 100644 tests/pos-java-interop/explicit-null-source/source.scala diff --git a/tests/neg/nullify.scala b/tests/neg/explicit-null-classfile.scala similarity index 89% rename from tests/neg/nullify.scala rename to tests/neg/explicit-null-classfile.scala index ad4bbee82d18..55341fd37aff 100644 --- a/tests/neg/nullify.scala +++ b/tests/neg/explicit-null-classfile.scala @@ -1,5 +1,5 @@ -// Test that we mark fields and methods in Java classes as nullable. +// Test that we mark fields and methods in Java classfiles as nullable. class Foo { def s[T](x: T | Null): T = x.asInstanceOf[T] diff --git a/tests/pos-java-interop/nullify.scala b/tests/pos-java-interop/explicit-null-classfile.scala similarity index 90% rename from tests/pos-java-interop/nullify.scala rename to tests/pos-java-interop/explicit-null-classfile.scala index cfd0bd630784..a069ac6a587e 100644 --- a/tests/pos-java-interop/nullify.scala +++ b/tests/pos-java-interop/explicit-null-classfile.scala @@ -1,5 +1,5 @@ -// Tests that we add "| Null" to Java fields and methods. +// Tests that we add "| Null" to Java fields and methods from classfiles. class Foo { // So that the chained accessors compile. diff --git a/tests/pos-java-interop/explicit-null-source/ArrayList.java b/tests/pos-java-interop/explicit-null-source/ArrayList.java new file mode 100644 index 000000000000..9b29e4401078 --- /dev/null +++ b/tests/pos-java-interop/explicit-null-source/ArrayList.java @@ -0,0 +1,6 @@ +class ArrayList { + ArrayList() {} + void add(T x) {} + T get(int x) { return null; } + Iterator iterator() { return new Iterator(); } +} diff --git a/tests/pos-java-interop/explicit-null-source/Iterator.java b/tests/pos-java-interop/explicit-null-source/Iterator.java new file mode 100644 index 000000000000..2afb13e586ae --- /dev/null +++ b/tests/pos-java-interop/explicit-null-source/Iterator.java @@ -0,0 +1,3 @@ +class Iterator { + Iterator() {} +} diff --git a/tests/pos-java-interop/explicit-null-source/source.scala b/tests/pos-java-interop/explicit-null-source/source.scala new file mode 100644 index 000000000000..c4814917a55d --- /dev/null +++ b/tests/pos-java-interop/explicit-null-source/source.scala @@ -0,0 +1,29 @@ + +// Tests that we add "| Null" to Java fields and methods from Java sources. +class Foo { + + // So that the chained accessors compile. + implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] + + def foo = { + import java.util.ArrayList + import java.util.Iterator + val x = new ArrayList[String]() + x.add(null) // | Null added to a method argument + val y = x.get(0) + if (y == null) { // | Null added to return type + } + + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() // test nested nullable containers + if (x3.get(0) == null) { + } + if (x3.get(0).get(0) == null) { + } + if (x3.get(0).get(0).get(0) == null) { + } + + val it = x3.iterator() // it: Iterator[Sting] | Null + if (it == null) { + } + } +} From dc9601d8964c5310cda847626a7c9764953adb0f Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 28 Aug 2018 16:57:31 -0400 Subject: [PATCH 010/127] Add JavaNull type JavaNull is defined as `type JavaNull = Null @JavaNullAnnot` On selections from an expression of type `T | JavaNull`, we select as if we were selecting from T. This is intended to make Java interop more user-friendly, because null values coming from Java are typed as `T | JavaNull`. Of course, this means selections on Java-retured values can fail with NPEs. --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 9 ++++++++- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + compiler/src/dotty/tools/dotc/core/Types.scala | 12 ++++++------ .../tools/dotc/core/classfile/ClassfileParser.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Namer.scala | 2 +- .../scala/annotation/internal/JavaNullAnnot.scala | 8 ++++++++ tests/pos-java-interop/t1101/S.scala | 2 +- 7 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 library/src/scala/annotation/internal/JavaNullAnnot.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6ceab97b9fe1..e560771a3cbc 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -3,7 +3,7 @@ package dotc package core import Types._, Contexts._, Symbols._, SymDenotations._, StdNames._, Names._ -import Flags._, Scopes._, Decorators._, NameOps._, Periods._ +import Flags._, Scopes._, Decorators._, NameOps._, Periods._, Annotations.Annotation import unpickleScala2.Scala2Unpickler.ensureConstructor import scala.collection.mutable import collection.mutable @@ -343,6 +343,11 @@ class Definitions { newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef + /** Marker for null values that originate in Java. + * Equivalently defined as `type JavaNull = Null @JavaNull` + */ + lazy val JavaNull = enterAliasType(tpnme.JavaNull, AnnotatedType.make(NullType, List(Annotation(JavaNullAnnot)))) + def JavaNullType = JavaNull.typeRef lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol @@ -816,6 +821,8 @@ class Definitions { def SetterMetaAnnot(implicit ctx: Context): ClassSymbol = SetterMetaAnnotType.symbol.asClass lazy val ShowAsInfixAnotType: TypeRef = ctx.requiredClassRef("scala.annotation.showAsInfix") def ShowAsInfixAnnot(implicit ctx: Context): ClassSymbol = ShowAsInfixAnotType.symbol.asClass + lazy val JavaNullAnnotType = ctx.requiredClassRef("scala.annotation.internal.JavaNullAnnot") + def JavaNullAnnot(implicit ctx: Context) = JavaNullAnnotType.symbol.asClass // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index f618a91271c6..bba5b97ad21a 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -196,6 +196,7 @@ object StdNames { final val Mirror: N = "Mirror" final val Nothing: N = "Nothing" final val Null: N = "Null" + final val JavaNull: N = "JavaNull" final val Object: N = "Object" final val PartialFunction: N = "PartialFunction" final val PrefixType: N = "PrefixType" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e53c4758719d..e2853c8d940a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4726,13 +4726,13 @@ object Types { protected def reapply(tp: Type): Type = apply(tp) } - /** Adds "| Null" to the relevant places of a Java type to reflect the fact + /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact * that Java types remain nullable by default. * - * nullify(T) = T | Null if T is a type parameter or class or interface - * nullify(C[S]) = C[nullify(S)] | Null if C is a generic class + * nullify(T) = T | JavaNull if T is a type parameter or class or interface + * nullify(C[S]) = C[nullify(S)] | JavaNull if C is a generic class */ - class NullifyMap(implicit ctx: Context) extends TypeMap { + class JavaNullMap(implicit ctx: Context) extends TypeMap { def shouldNullify(tp: TypeRef): Boolean = { !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) } @@ -4744,9 +4744,9 @@ object Types { case tp: TypeAlias => mapOver(tp) case tp: TypeRef if shouldNullify(tp) => - OrType(tp, defn.NullType) + OrType(tp, defn.JavaNullType) case tp@RefinedType(parent, name, info) => - OrType(derivedRefinedType(tp, parent, this(info)), defn.NullType) + OrType(derivedRefinedType(tp, parent, this(info)), defn.JavaNullType) case _ => tp } diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 36bf3f7c0802..b8cdee2e5741 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -269,7 +269,7 @@ class ClassfileParser( setPrivateWithin(denot, jflags) denot.info = translateTempPoly(parseAttributes(sym, denot.info)) if (isConstructor) normalizeConstructorInfo() - val nullMap = new NullifyMap + val nullMap = new JavaNullMap if (!isConstructor) denot.info = nullMap(denot.info) if ((denot is Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0) denot.info = arrayToRepeated(denot.info) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 066325492871..ad48c9369172 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1179,7 +1179,7 @@ class Namer { typer: Typer => val resTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) val isConstructor = mdef.name == nme.CONSTRUCTOR if (mdef.mods.is(JavaDefined) && !isConstructor) { - val nullMap = new NullifyMap + val nullMap = new JavaNullMap nullMap(resTpe) } else { resTpe diff --git a/library/src/scala/annotation/internal/JavaNullAnnot.scala b/library/src/scala/annotation/internal/JavaNullAnnot.scala new file mode 100644 index 000000000000..3036b0fec154 --- /dev/null +++ b/library/src/scala/annotation/internal/JavaNullAnnot.scala @@ -0,0 +1,8 @@ +package scala.annotation.internal + +import scala.annotation.Annotation + +/** Marks null values originating in Java. + * TODO(abeln): give example + */ +final class JavaNullAnnot() extends Annotation diff --git a/tests/pos-java-interop/t1101/S.scala b/tests/pos-java-interop/t1101/S.scala index af7a591e589c..7d7afc5704a0 100644 --- a/tests/pos-java-interop/t1101/S.scala +++ b/tests/pos-java-interop/t1101/S.scala @@ -1 +1 @@ -class S { val x: J.E = null; System.out.println(J.E.E1) } +class S { val x: J.E|Null = null; System.out.println(J.E.E1) } From 1217b9bdcfcdfc1f2e0a126d352956494efa05d1 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 28 Aug 2018 17:46:51 -0400 Subject: [PATCH 011/127] Nullify applied types too --- compiler/src/dotty/tools/dotc/core/Types.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e2853c8d940a..e4a39bbe157e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4734,10 +4734,13 @@ object Types { */ class JavaNullMap(implicit ctx: Context) extends TypeMap { def shouldNullify(tp: TypeRef): Boolean = { - !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) + val res = !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) +// println(s"tp = ${tp.show} res = ${res}") + res } override def apply(tp: Type): Type = { +// println(s"tp = ${tp.show} ${tp}") tp match { case tp: MethodType => mapOver(tp) @@ -4747,6 +4750,8 @@ object Types { OrType(tp, defn.JavaNullType) case tp@RefinedType(parent, name, info) => OrType(derivedRefinedType(tp, parent, this(info)), defn.JavaNullType) + case AppliedType(tycons, targs) => + OrType(AppliedType(tycons, targs map this), defn.JavaNullType) case _ => tp } From 16e4af880a532c9ca388266b62234e2fddbf4920 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 29 Aug 2018 11:41:50 -0400 Subject: [PATCH 012/127] Handle JavaNull when converting varargs When the compiler encounters a method with a varargs argument, the type of the varargs is initially represnted as an Array[T]. Later, it is transformed into a RepeatedParamType[T]. However, the nullability transform makes it so that the varargs has type `Array[T|JavaNull]|JavaNull`. We need to teach `arrayToRepeated` how to handle that case so we can get `RepeatedParamType[T|JavaNull]|JavaNull` as the result. --- .../dotty/tools/dotc/core/Definitions.scala | 2 ++ .../src/dotty/tools/dotc/core/Types.scala | 29 ++++++++++++++++--- .../core/unpickleScala2/Scala2Unpickler.scala | 9 ++++-- .../explicit-null-classfile.scala | 24 ++++----------- 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e560771a3cbc..4483dba275b4 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -349,6 +349,8 @@ class Definitions { lazy val JavaNull = enterAliasType(tpnme.JavaNull, AnnotatedType.make(NullType, List(Annotation(JavaNullAnnot)))) def JavaNullType = JavaNull.typeRef + def javaNullable(tp: Type) = if (tp.isJavaNullable) tp else OrType(tp, JavaNullType) + lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e4a39bbe157e..f78517b49fb9 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -276,6 +276,13 @@ object Types { case _ => false } + /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ + def isJavaNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeJavaNull match { + case OrType(_, right) if right == defn.JavaNullType => + true + case _ => false + } + /** Is this type produced as a repair for an error? */ final def isError(implicit ctx: Context): Boolean = stripTypeVar.isInstanceOf[ErrorType] @@ -964,6 +971,20 @@ object Types { case _ => this } + /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ + def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeJavaNull match { + case OrType(left, right) if right == defn.JavaNullType => + left + case _ => this + } + + /** Converts types of the form `JavaNull | T` to `T | JavaNull`. Does not do any widening or dealiasing. */ + def normalizeJavaNull(implicit ctx: Context): Type = this match { + case OrType(left, right) if left == defn.JavaNullType => + OrType(right, left) + case _ => this + } + /** Widen from singleton type to its underlying non-singleton * base type by applying one or more `underlying` dereferences, * Also go from => T to T. @@ -4747,11 +4768,11 @@ object Types { case tp: TypeAlias => mapOver(tp) case tp: TypeRef if shouldNullify(tp) => - OrType(tp, defn.JavaNullType) - case tp@RefinedType(parent, name, info) => - OrType(derivedRefinedType(tp, parent, this(info)), defn.JavaNullType) + defn.javaNullable(tp) + case tp@RefinedType(parent, name, info) => // TODO(abeln): does this ever happen for Java-sourced types? + defn.javaNullable(derivedRefinedType(tp, parent, this(info))) case AppliedType(tycons, targs) => - OrType(AppliedType(tycons, targs map this), defn.JavaNullType) + defn.javaNullable(AppliedType(tycons, targs map this)) case _ => tp } diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index d1297623da83..e31288d4e16d 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -62,8 +62,9 @@ object Scala2Unpickler { def arrayToRepeated(tp: Type)(implicit ctx: Context): Type = tp match { case tp: MethodType => val lastArg = tp.paramInfos.last - assert(lastArg isRef defn.ArrayClass) - val elemtp0 :: Nil = lastArg.baseType(defn.ArrayClass).argInfos + val lastArg1 = lastArg.stripJavaNull + assert(lastArg1 isRef defn.ArrayClass) + val elemtp0 :: Nil = lastArg1.baseType(defn.ArrayClass).argInfos val elemtp = elemtp0 match { case AndType(t1, t2) => // drop intersection with Object for abstract types an parameters in varargs. Erasure can handle them. if (t2.isRef(defn.ObjectClass)) @@ -76,9 +77,11 @@ object Scala2Unpickler { case _ => elemtp0 } + val repParamTp = defn.RepeatedParamType.appliedTo(elemtp) + val lastParamTp = if (lastArg.isJavaNullable) defn.javaNullable(repParamTp) else repParamTp tp.derivedLambdaType( tp.paramNames, - tp.paramInfos.init :+ defn.RepeatedParamType.appliedTo(elemtp), + tp.paramInfos.init :+ lastParamTp, tp.resultType) case tp: PolyType => tp.derivedLambdaType(tp.paramNames, tp.paramInfos, arrayToRepeated(tp.resultType)) diff --git a/tests/pos-java-interop/explicit-null-classfile.scala b/tests/pos-java-interop/explicit-null-classfile.scala index a069ac6a587e..fa65bf6bc55e 100644 --- a/tests/pos-java-interop/explicit-null-classfile.scala +++ b/tests/pos-java-interop/explicit-null-classfile.scala @@ -1,29 +1,15 @@ -// Tests that we add "| Null" to Java fields and methods from classfiles. +// Tests that we add "| JavaNull" to Java fields and methods from classfiles. class Foo { - // So that the chained accessors compile. - implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] - def foo = { import java.util.ArrayList import java.util.Iterator val x = new ArrayList[String]() - x.add(null) // | Null added to a method argument - val y = x.get(0) - if (y == null) { // | Null added to return type - } - - val x3 = new ArrayList[ArrayList[ArrayList[String]]]() // test nested nullable containers - if (x3.get(0) == null) { - } - if (x3.get(0).get(0) == null) { - } - if (x3.get(0).get(0).get(0) == null) { - } + x.add(null) // | JavaNull added to a method argument - val it = x3.iterator() // it: Iterator[Sting] | Null - if (it == null) { - } + // Test that we can select through "|JavaNull" (unsoundly). + val x2 = new ArrayList[ArrayList[ArrayList[String]]]() + x2.get(0).get(0) } } From 1f991386a12971826f13c4f26d40d614de5adb22 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 29 Aug 2018 16:22:24 -0400 Subject: [PATCH 013/127] Use helper methods when typing Select trees --- compiler/src/dotty/tools/dotc/core/Types.scala | 13 ++++++++++--- .../src/dotty/tools/dotc/typer/ErrorReporting.scala | 4 +++- .../pos-java-interop/explicit-null-classfile.scala | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f78517b49fb9..d1c27d9cfb06 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1002,9 +1002,14 @@ object Types { /** Widen from singleton type to its underlying non-singleton * base type by applying one or more `underlying` dereferences. */ - final def widenSingleton(implicit ctx: Context): Type = stripTypeVar.stripAnnots match { - case tp: SingletonType if !tp.isOverloaded => tp.underlying.widenSingleton - case _ => this + final def widenSingleton(implicit ctx: Context): Type = { +// println(s"widenSingleton ${this.show}") + stripTypeVar.stripAnnots match { + case tp: SingletonType if !tp.isOverloaded => +// println(s"underlying = ${tp.underlying}") + tp.underlying.widenSingleton + case _ => this + } } /** Widen from TermRef to its underlying non-termref @@ -2234,6 +2239,8 @@ object Types { //assert(name.toString != "") override def underlying(implicit ctx: Context): Type = { val d = denot +// println(s"${d.show}") +// println(s"underlying isoverloaded = ${d.isOverloaded} d.info = ${d.info}") if (d.isOverloaded) NoType else d.info } diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index e9f9d6d5ea50..ea359213cf80 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -79,8 +79,10 @@ object ErrorReporting { def takesNoParamsStr(tree: Tree, kind: String): String = if (tree.tpe.widen.exists) i"${exprStr(tree)} does not take ${kind}parameters" - else + else { +// println(s"tree.tpe.widen = ${tree.tpe.widen.show}") i"undefined: $tree # ${tree.uniqueId}: ${tree.tpe.toString} at ${ctx.phase}" + } def patternConstrStr(tree: Tree): String = ??? diff --git a/tests/pos-java-interop/explicit-null-classfile.scala b/tests/pos-java-interop/explicit-null-classfile.scala index fa65bf6bc55e..75a6160249d4 100644 --- a/tests/pos-java-interop/explicit-null-classfile.scala +++ b/tests/pos-java-interop/explicit-null-classfile.scala @@ -10,6 +10,6 @@ class Foo { // Test that we can select through "|JavaNull" (unsoundly). val x2 = new ArrayList[ArrayList[ArrayList[String]]]() - x2.get(0).get(0) + val x3 = x2.get(0).get(0) } } From 01300e0e6b8df361c83ba41fd89c054c4bc530a1 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 30 Aug 2018 12:41:28 -0400 Subject: [PATCH 014/127] Better implementation of member selection for JavaNull Instead of changing typedSelect, add the special case for JavaNull in Types#findMember. Additionally, cleaned up the tests, which now pass without -Ychecks but fail with -Ychecks. Additionally, there's a problem where the compiler won't infer a union type: e.g. ``` val x = new ArrayList[String]() val r = x.get(0) ``` The compiler will infer `r: Object` and not `r: String|JavaNull`. Need to address separately. --- .../src/dotty/tools/dotc/core/Types.scala | 40 ++++++++++--------- tests/neg/explicit-null-classfile.scala | 20 ++++++---- .../explicit-null-classfile.scala | 10 +++-- .../explicit-null-source/source.scala | 28 ++++--------- 4 files changed, 46 insertions(+), 52 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d1c27d9cfb06..a0bd181c5765 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -276,10 +276,12 @@ object Types { case _ => false } + /** Is this type `JavaNullType` */ + def isJavaNull(implicit ctx: Context): Boolean = this == defn.JavaNullType + /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ def isJavaNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeJavaNull match { - case OrType(_, right) if right == defn.JavaNullType => - true + case OrType(_, right) if right.isJavaNull => true case _ => false } @@ -590,11 +592,17 @@ object Types { case AndType(l, r) => goAnd(l, r) case tp: OrType => - // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` - // achieved that by narrowing `pre` to each alternative, but it led to merge errors in - // lots of places. The present strategy is instead of widen `tp` using `join` to be a - // supertype of `pre`. - go(tp.join) + if (tp.isJavaNullable) { + // We need to strip JavaNull from both the type and the prefix so that + // `pre <: tp` continues to hold. + tp.stripJavaNull.findMember(name, pre.stripJavaNull, required, excluded) + } else { + // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` + // achieved that by narrowing `pre` to each alternative, but it led to merge errors in + // lots of places. The present strategy is instead of widen `tp` using `join` to be a + // supertype of `pre`. + go(tp.join) + } case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, required, excluded) case err: ErrorType => @@ -973,16 +981,16 @@ object Types { /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeJavaNull match { - case OrType(left, right) if right == defn.JavaNullType => - left + case OrType(left, right) if right.isJavaNull => left.stripJavaNull case _ => this } /** Converts types of the form `JavaNull | T` to `T | JavaNull`. Does not do any widening or dealiasing. */ - def normalizeJavaNull(implicit ctx: Context): Type = this match { - case OrType(left, right) if left == defn.JavaNullType => - OrType(right, left) - case _ => this + def normalizeJavaNull(implicit ctx: Context): Type = { + this match { + case OrType(left, right) if left.isJavaNull => OrType(right, left) + case _ => this + } } /** Widen from singleton type to its underlying non-singleton @@ -1003,10 +1011,8 @@ object Types { * base type by applying one or more `underlying` dereferences. */ final def widenSingleton(implicit ctx: Context): Type = { -// println(s"widenSingleton ${this.show}") stripTypeVar.stripAnnots match { case tp: SingletonType if !tp.isOverloaded => -// println(s"underlying = ${tp.underlying}") tp.underlying.widenSingleton case _ => this } @@ -2239,8 +2245,6 @@ object Types { //assert(name.toString != "") override def underlying(implicit ctx: Context): Type = { val d = denot -// println(s"${d.show}") -// println(s"underlying isoverloaded = ${d.isOverloaded} d.info = ${d.info}") if (d.isOverloaded) NoType else d.info } @@ -4763,12 +4767,10 @@ object Types { class JavaNullMap(implicit ctx: Context) extends TypeMap { def shouldNullify(tp: TypeRef): Boolean = { val res = !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) -// println(s"tp = ${tp.show} res = ${res}") res } override def apply(tp: Type): Type = { -// println(s"tp = ${tp.show} ${tp}") tp match { case tp: MethodType => mapOver(tp) diff --git a/tests/neg/explicit-null-classfile.scala b/tests/neg/explicit-null-classfile.scala index 55341fd37aff..3f848e27d9be 100644 --- a/tests/neg/explicit-null-classfile.scala +++ b/tests/neg/explicit-null-classfile.scala @@ -2,19 +2,23 @@ // Test that we mark fields and methods in Java classfiles as nullable. class Foo { - def s[T](x: T | Null): T = x.asInstanceOf[T] - def foo = { import java.util.ArrayList val x = new ArrayList[String]() - x.add("Hello") // Allowed since `add` takes String | Null as argument + x.add("Hello") // Allowed since `add` takes String|JavaNull as argument x.add(null) - val r: String = x.get(0) // error: got String | Null instead of String + val r: String = x.get(0) // error: got String|JavaNull instead of String + + val x2 = new ArrayList[Int]() + val r2: Int = x2.get(0) // error: even though Int is non-nullable in Scala, its counterpart + // (for purposes of generics) in Java (Integer) is. So we're missing |JavaNull + + // Test that as we extract return values, we're missing the |JavaNull in the return type. + // i.e. test that the nullability is propagated to nested containers. val ll = new ArrayList[ArrayList[ArrayList[String]]] val level1: ArrayList[ArrayList[String]] = ll.get(0) // error - val level2: ArrayList[String] = s(ll.get(0)).get(0) // error - val level3: String = s(s(ll.get(0)).get(0)).get(0) // error - val ok: String = s(s(s(ll.get(0)).get(0)).get(0)) + val level2: ArrayList[String] = ll.get(0).get(0) // error + val level3: String = ll.get(0).get(0).get(0) // error + val ok: String = ll.get(0).get(0).get(0) // error } - } diff --git a/tests/pos-java-interop/explicit-null-classfile.scala b/tests/pos-java-interop/explicit-null-classfile.scala index 75a6160249d4..6a9d7a80372b 100644 --- a/tests/pos-java-interop/explicit-null-classfile.scala +++ b/tests/pos-java-interop/explicit-null-classfile.scala @@ -1,15 +1,17 @@ -// Tests that we add "| JavaNull" to Java fields and methods from classfiles. +// Tests that we add "|JavaNull" to Java fields and methods from classfiles. class Foo { def foo = { import java.util.ArrayList import java.util.Iterator val x = new ArrayList[String]() - x.add(null) // | JavaNull added to a method argument + x.add(null) // |JavaNull added to method argument + // TODO(abeln): we can't directly test that |JavaNull was added to the return type + // in a positive test. We test it in the negative counterpart instead. // Test that we can select through "|JavaNull" (unsoundly). - val x2 = new ArrayList[ArrayList[ArrayList[String]]]() - val x3 = x2.get(0).get(0) + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() } } diff --git a/tests/pos-java-interop/explicit-null-source/source.scala b/tests/pos-java-interop/explicit-null-source/source.scala index c4814917a55d..81be19f9b8e5 100644 --- a/tests/pos-java-interop/explicit-null-source/source.scala +++ b/tests/pos-java-interop/explicit-null-source/source.scala @@ -1,29 +1,15 @@ -// Tests that we add "| Null" to Java fields and methods from Java sources. +// Tests that we add "|JavaNull" to Java fields and methods from classfiles. class Foo { - // So that the chained accessors compile. - implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] - def foo = { - import java.util.ArrayList - import java.util.Iterator val x = new ArrayList[String]() - x.add(null) // | Null added to a method argument - val y = x.get(0) - if (y == null) { // | Null added to return type - } - - val x3 = new ArrayList[ArrayList[ArrayList[String]]]() // test nested nullable containers - if (x3.get(0) == null) { - } - if (x3.get(0).get(0) == null) { - } - if (x3.get(0).get(0).get(0) == null) { - } + x.add(null) // |JavaNull added to method argument + // TODO(abeln): we can't directly test that |JavaNull was added to the return type + // in a positive test. We test it in the negative counterpart instead. - val it = x3.iterator() // it: Iterator[Sting] | Null - if (it == null) { - } + // Test that we can select through "|JavaNull" (unsoundly). + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() } } From 57013927f0f3ff092d25e9a818e441d452d210b7 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 30 Aug 2018 14:59:47 -0400 Subject: [PATCH 015/127] Teach FirstTransform that |JavaNull is see-through Local unit tests now pass. --- .../src/dotty/tools/dotc/transform/FirstTransform.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 6717b3499f05..cc0fb3b27b76 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -50,10 +50,11 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = { tree match { case Select(qual, name) if !name.is(OuterSelectName) && tree.symbol.exists => + val qualTpe = if (qual.tpe.isJavaNullable) qual.tpe.stripJavaNull else qual.tpe assert( - qual.tpe.derivesFrom(tree.symbol.owner) || - tree.symbol.is(JavaStatic) && qual.tpe.derivesFrom(tree.symbol.enclosingClass), - i"non member selection of ${tree.symbol.showLocated} from ${qual.tpe} in $tree") + qualTpe.derivesFrom(tree.symbol.owner) || + tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass), + i"non member selection of ${tree.symbol.showLocated} from ${qualTpe} in $tree") case _: TypeTree => case _: Import | _: NamedArg | _: TypTree => assert(false, i"illegal tree: $tree") From f6d116b9c6580340272a096d363fff6ceefde61d Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 30 Aug 2018 16:01:05 -0400 Subject: [PATCH 016/127] Improve nullability transform Tag as nullable TypeParamRefs, so we can handle polymorphic Java methods. --- compiler/src/dotty/tools/dotc/config/Printers.scala | 1 + tests/neg/explicit-null-classfile.scala | 5 +++++ tests/pos-java-interop/explicit-null-classfile.scala | 3 +++ 3 files changed, 9 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 74739b612ff9..56ec535aa368 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -39,4 +39,5 @@ object Printers { val typr: Printer = noPrinter val unapp: Printer = noPrinter val variances: Printer = noPrinter + val nullability: Printer = noPrinter } diff --git a/tests/neg/explicit-null-classfile.scala b/tests/neg/explicit-null-classfile.scala index 3f848e27d9be..14cac09a3822 100644 --- a/tests/neg/explicit-null-classfile.scala +++ b/tests/neg/explicit-null-classfile.scala @@ -20,5 +20,10 @@ class Foo { val level2: ArrayList[String] = ll.get(0).get(0) // error val level3: String = ll.get(0).get(0).get(0) // error val ok: String = ll.get(0).get(0).get(0) // error + + // Test that return values in PolyTypes are marked as nullable. + val lstring = new ArrayList[String]() + val res: String = java.util.Collections.max(lstring) // error: missing |Null + val res2: String|Null = java.util.Collections.max(lstring) // ok } } diff --git a/tests/pos-java-interop/explicit-null-classfile.scala b/tests/pos-java-interop/explicit-null-classfile.scala index 6a9d7a80372b..fc4b4d1d06f0 100644 --- a/tests/pos-java-interop/explicit-null-classfile.scala +++ b/tests/pos-java-interop/explicit-null-classfile.scala @@ -13,5 +13,8 @@ class Foo { // Test that we can select through "|JavaNull" (unsoundly). val x3 = new ArrayList[ArrayList[ArrayList[String]]]() val x4: Int = x3.get(0).get(0).get(0).length() + + // Test that generic methods are also tagged as nullable. + java.util.Collections.max(null) } } From eda351bd1a90ea7319d802045476f378c0e851f5 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 30 Aug 2018 17:50:31 -0400 Subject: [PATCH 017/127] Pass empty string instead of null in dummy constructor When an enum is read from Java code, the compiler synthesizes a bunch of classes/modules/fields for it. One of the synthesized entities is a class that extends java.lang.Enum and calls its constructor. The first argument of the constructor is a string, so we were passing null which failed. Pass the empty string instead. This is ok because the synthesized Java code isn't run: it's just there for typechecking. --- compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index e16d27ce06af..79f9808d2738 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -836,7 +836,7 @@ object JavaParsers { */ val superclazz = Apply(TypeApply( Select(New(javaLangDot(tpnme.Enum)), nme.CONSTRUCTOR), List(enumType)), - List(Literal(Constant(null)),Literal(Constant(0)))) + List(Literal(Constant("")),Literal(Constant(0)))) val enumclazz = atPos(start, nameOffset) { TypeDef(name, makeTemplate(superclazz :: interfaces, body, List(), true)).withMods(mods | Flags.JavaEnum) From 8ffaea3d7442cf132c771ca3798e3550fafe8139 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 30 Aug 2018 18:04:05 -0400 Subject: [PATCH 018/127] Moar easy test fixes --- tests/pos-java-interop/1576/Test.scala | 2 +- tests/pos-java-interop/t0695/Test.scala | 2 +- tests/pos-java-interop/t1409/ConcreteImpl.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos-java-interop/1576/Test.scala b/tests/pos-java-interop/1576/Test.scala index ea3d06a83005..f87194f141ed 100644 --- a/tests/pos-java-interop/1576/Test.scala +++ b/tests/pos-java-interop/1576/Test.scala @@ -1,5 +1,5 @@ object Test { - val v: TagAnnotation = null + val v: TagAnnotation = ??? println(v.value) // error: value value in class TagAnnotation cannot be accessed as a // member of TagAnnotation(Test.v) from module class Test$. } diff --git a/tests/pos-java-interop/t0695/Test.scala b/tests/pos-java-interop/t0695/Test.scala index 7318867bf7c7..3269f9326a66 100644 --- a/tests/pos-java-interop/t0695/Test.scala +++ b/tests/pos-java-interop/t0695/Test.scala @@ -1,3 +1,3 @@ object Test extends JavaClass[AnyRef] { - var field: InnerClass = null + var field: InnerClass = ??? } diff --git a/tests/pos-java-interop/t1409/ConcreteImpl.scala b/tests/pos-java-interop/t1409/ConcreteImpl.scala index d427e957e5af..339259594cc6 100644 --- a/tests/pos-java-interop/t1409/ConcreteImpl.scala +++ b/tests/pos-java-interop/t1409/ConcreteImpl.scala @@ -1,3 +1,3 @@ class ConcreteImpl extends AbstractImpl { - def create : OuterInterface.InnerInterface = null + def create : OuterInterface.InnerInterface = ??? } From e0d46d95743830995ca20f338c4d62dc66ed602e Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 31 Aug 2018 15:31:27 -0400 Subject: [PATCH 019/127] Make reference types nullable again after erasure Before erasure, reference types are non-nullable, but after it they should be nullable again, because JVM types are nullable. This fixes tests/pos/i536 by changing the notion of a nullable type to take into consideration the current phase id. A similar thing is already done in TypeComparer in a different case: https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L676 --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 12 ++++++++++-- .../src/dotty/tools/dotc/core/SymDenotations.scala | 8 ++++++-- tests/pos/i536.scala | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4483dba275b4..15fedcef07a7 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -971,8 +971,16 @@ class Definitions { name.length > prefix.length && name.drop(prefix.length).forall(_.isDigit)) - def isBottomClass(cls: Symbol) = cls == NothingClass - def isBottomType(tp: Type) = tp.derivesFrom(NothingClass) + def isBottomClass(cls: Symbol) = { + // After erasure, reference types become nullable again. + if (ctx.phaseId <= ctx.erasurePhase.id) cls == NothingClass + else cls == NothingClass || cls == NullClass + } + def isBottomType(tp: Type) = { + // After erasure, reference types become nullable again. + if (ctx.phaseId <= ctx.erasurePhase.id) tp.derivesFrom(NothingClass) + else tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass) + } /** Is a function class. * - FunctionN for N >= 0 diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 5b98be795645..1d045ae2e750 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -681,8 +681,12 @@ object SymDenotations { } /** Is this symbol a class with nullable values? */ - final def isNullableClass(implicit ctx: Context): Boolean = - symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass + final def isNullableClass(implicit ctx: Context): Boolean = { + // After erasure, reference types become nullable again. + if (ctx.phaseId <= ctx.erasurePhase.id) symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass + else isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass + } + /** Is this definition accessible as a member of tree with type `pre`? * @param pre The type of the tree from which the selection is made diff --git a/tests/pos/i536.scala b/tests/pos/i536.scala index 49d8f6058be3..1ea41d9e9da9 100644 --- a/tests/pos/i536.scala +++ b/tests/pos/i536.scala @@ -2,11 +2,11 @@ trait Comp[T] trait Coll[T] class C extends Comp[C] object Max { - def max[M <: Comp[_ >: M]](x: Coll[_ <: M] | Null): M = ??? + def max[M <: Comp[_ >: M]](x: Coll[_ <: M]): M = ??? def max[M](x: Coll[_ <: M], cmp: Object): M = ??? val xs: Coll[C] = ??? val m1 = max(xs) - val m2 = max(null) + val m2 = max(???) java.util.Collections.max(null) } From 8c83873ff809caf07268299143bb227f07c720c6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 3 Sep 2018 12:46:56 -0400 Subject: [PATCH 020/127] Fix #5067: Ycheck failure in pattern matching against a value of type `Nothing` When desugaring pattern matching code for expressions where the matched value has type `Null` or `Nothing`, we used to generate code that's type-incorrect. Example: ``` val Some(x) = null ``` got desugared into ``` val x: Nothing = matchResult1[Nothing]: { case val x1: Null @unchecked = null: Null @unchecked if x1.ne(null) then { case val x: Nothing = x1.value.asInstanceOf[Nothing] return[matchResult1] x: Nothing } else () return[matchResult1] throw new MatchError(x1) } ``` There were two problems here: 1) `x1.ne(null)` 2) `x1.value` In both cases, we're trying to invoke methods that don't exist for type `Nothing` (and #2 doesn't exist for `Null`). This commit changes the desugaring so we generate a no-op for unapply when the value matched has type `Nothing` or `Null`. This works because the code we used to generate is never executed (because the `x1.ne(null)`) check. --- .../src/dotty/tools/dotc/core/Types.scala | 6 ++++ .../tools/dotc/transform/PatternMatcher.scala | 24 ++++++++++++-- tests/pos/i5067.scala | 8 +++++ tests/run/i5067.check | 1 + tests/run/i5067b.check | 3 ++ tests/run/i5067b.scala | 33 +++++++++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 tests/pos/i5067.scala create mode 100644 tests/run/i5067.check create mode 100644 tests/run/i5067b.check create mode 100644 tests/run/i5067b.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index a0bd181c5765..955458a421b0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -236,6 +236,12 @@ object Types { } } + /** Is this type exactly Null (no vars, aliases, refinements etc allowed)? */ + def isNullType(implicit ctx: Context): Boolean = this match { + case tp: TypeRef => tp.symbol eq defn.NullClass + case _ => false + } + /** Is this type exactly Nothing (no vars, aliases, refinements etc allowed)? */ def isBottomType(implicit ctx: Context): Boolean = this match { case tp: TypeRef => tp.symbol eq defn.NothingClass diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index e470af9c43cb..2b59ccd209bc 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -145,6 +145,7 @@ object PatternMatcher { case class ReturnPlan(var label: TermSymbol) extends Plan case class SeqPlan(var head: Plan, var tail: Plan) extends Plan case class ResultPlan(var tree: Tree) extends Plan + case object NoOpPlan extends Plan object TestPlan { def apply(test: Test, sym: Symbol, pos: Position, ons: Plan): TestPlan = @@ -338,9 +339,19 @@ object PatternMatcher { val mt @ MethodType(_) = extractor.tpe.widen var unapp = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head)) if (implicits.nonEmpty) unapp = unapp.appliedToArgs(implicits) - val unappPlan = unapplyPlan(unapp, args) - if (scrutinee.info.isNotNull || nonNull(scrutinee)) unappPlan - else TestPlan(NonNullTest, scrutinee, tree.pos, unappPlan) + val scrutineeTpe = scrutinee.info.widenDealias + if (scrutineeTpe.isBottomType || scrutineeTpe.isNullType) { + // If the value being matched against has type `Nothing`, then the unapply code + // will never execute. + // If it has type `Null`, then the `NonNullTest` below guarantees that the unapply code + // won't execute either. + // So we don't need a plan in this case. + NoOpPlan + } else { + val unappPlan = unapplyPlan(unapp, args) + if (scrutinee.info.isNotNull || nonNull(scrutinee)) unappPlan + else TestPlan(NonNullTest, scrutinee, tree.pos, unappPlan) + } case Bind(name, body) => if (name == nme.WILDCARD) patternPlan(scrutinee, body, onSuccess) else { @@ -420,6 +431,7 @@ object PatternMatcher { case plan: ReturnPlan => apply(plan) case plan: SeqPlan => apply(plan) case plan: ResultPlan => plan + case NoOpPlan => NoOpPlan } } @@ -696,6 +708,7 @@ object PatternMatcher { private def canFallThrough(plan: Plan): Boolean = plan match { case _:ReturnPlan | _:ResultPlan => false case _:TestPlan | _:LabeledPlan => true + case NoOpPlan => true case LetPlan(_, body) => canFallThrough(body) case SeqPlan(_, tail) => canFallThrough(tail) } @@ -859,6 +872,9 @@ object PatternMatcher { case ResultPlan(tree) => if (tree.tpe <:< defn.NothingType) tree // For example MatchError else Return(tree, ref(resultLabel)) + case NoOpPlan => + // TODO(abeln): optimize away + Literal(Constant(true)) } } @@ -897,6 +913,8 @@ object PatternMatcher { showPlan(tail) case ResultPlan(tree) => sb.append(tree.show) + case NoOpPlan => + sb.append("NoOp") } } showPlan(plan) diff --git a/tests/pos/i5067.scala b/tests/pos/i5067.scala new file mode 100644 index 000000000000..6128a9acaa27 --- /dev/null +++ b/tests/pos/i5067.scala @@ -0,0 +1,8 @@ +class Foo { + val Some(_) = ??? + val (_, _, _) = ??? + ??? match { + case Some(_) => () + case (_, _, _) => () + } +} diff --git a/tests/run/i5067.check b/tests/run/i5067.check new file mode 100644 index 000000000000..ee69e2a9b9f7 --- /dev/null +++ b/tests/run/i5067.check @@ -0,0 +1 @@ +matches null literal diff --git a/tests/run/i5067b.check b/tests/run/i5067b.check new file mode 100644 index 000000000000..be60186f7c25 --- /dev/null +++ b/tests/run/i5067b.check @@ -0,0 +1,3 @@ +match error +match error nested +not implemented error diff --git a/tests/run/i5067b.scala b/tests/run/i5067b.scala new file mode 100644 index 000000000000..36ff84478248 --- /dev/null +++ b/tests/run/i5067b.scala @@ -0,0 +1,33 @@ +object Test { + def main(args: Array[String]): Unit = { + class B[T] {} + object B { + def unapply[T](x: Any): Option[B[T]] = None + } + try { + val B(_) = null + } catch { + case e: MatchError => println("match error") + } + + null match { + case null => + try { + null match { + case Some(_) => () + } + } catch { + case e: MatchError => println("match error nested") + } + } + + try { + ??? match { + case (_, _) => () + case _ => () + } + } catch { + case e: NotImplementedError => println("not implemented error") + } + } +} From 9ee807cb1574e09e8cf956fd749df5ff14738da7 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 7 Sep 2018 13:39:56 -0400 Subject: [PATCH 021/127] Breakup test suite into smaller tests --- tests/neg/explicit-null-classfile.scala | 29 ------------------- tests/neg/explicit-null-default.scala | 17 +++++++++++ .../neg/explicit-null-interop-javanull.scala | 8 +++++ .../neg/explicit-null-interop-polytypes.scala | 7 +++++ .../neg/explicit-null-interop-propagate.scala | 11 +++++++ tests/neg/explicit-null-interop-return.scala | 14 +++++++++ .../explicit-null-classfile.scala | 20 ------------- .../explicit-null-source/ArrayList.java | 6 ---- .../explicit-null-source/Iterator.java | 3 -- .../explicit-null-source/source.scala | 15 ---------- .../J.java | 5 ++++ .../S.scala | 5 ++++ .../explicit-null-interop-constructor.scala | 6 ++++ .../explicit-null-interop-javanull-src/J.java | 8 +++++ .../S.scala | 6 ++++ .../pos/explicit-null-interop-javanull.scala | 10 +++++++ tests/pos/explicit-null-interop-static/J.java | 4 +++ .../pos/explicit-null-interop-static/S.scala | 6 ++++ .../explicit-null-interop-valuetypes.scala | 6 ++++ tests/pos/explicit-null-throw-null.scala | 4 +++ tests/pos/explicit-null-union.scala | 4 +++ 21 files changed, 121 insertions(+), 73 deletions(-) delete mode 100644 tests/neg/explicit-null-classfile.scala create mode 100644 tests/neg/explicit-null-default.scala create mode 100644 tests/neg/explicit-null-interop-javanull.scala create mode 100644 tests/neg/explicit-null-interop-polytypes.scala create mode 100644 tests/neg/explicit-null-interop-propagate.scala create mode 100644 tests/neg/explicit-null-interop-return.scala delete mode 100644 tests/pos-java-interop/explicit-null-classfile.scala delete mode 100644 tests/pos-java-interop/explicit-null-source/ArrayList.java delete mode 100644 tests/pos-java-interop/explicit-null-source/Iterator.java delete mode 100644 tests/pos-java-interop/explicit-null-source/source.scala create mode 100644 tests/pos/explicit-null-interop-constructor-src/J.java create mode 100644 tests/pos/explicit-null-interop-constructor-src/S.scala create mode 100644 tests/pos/explicit-null-interop-constructor.scala create mode 100644 tests/pos/explicit-null-interop-javanull-src/J.java create mode 100644 tests/pos/explicit-null-interop-javanull-src/S.scala create mode 100644 tests/pos/explicit-null-interop-javanull.scala create mode 100644 tests/pos/explicit-null-interop-static/J.java create mode 100644 tests/pos/explicit-null-interop-static/S.scala create mode 100644 tests/pos/explicit-null-interop-valuetypes.scala create mode 100644 tests/pos/explicit-null-throw-null.scala create mode 100644 tests/pos/explicit-null-union.scala diff --git a/tests/neg/explicit-null-classfile.scala b/tests/neg/explicit-null-classfile.scala deleted file mode 100644 index 14cac09a3822..000000000000 --- a/tests/neg/explicit-null-classfile.scala +++ /dev/null @@ -1,29 +0,0 @@ - -// Test that we mark fields and methods in Java classfiles as nullable. -class Foo { - - def foo = { - import java.util.ArrayList - val x = new ArrayList[String]() - x.add("Hello") // Allowed since `add` takes String|JavaNull as argument - x.add(null) - val r: String = x.get(0) // error: got String|JavaNull instead of String - - val x2 = new ArrayList[Int]() - val r2: Int = x2.get(0) // error: even though Int is non-nullable in Scala, its counterpart - // (for purposes of generics) in Java (Integer) is. So we're missing |JavaNull - - // Test that as we extract return values, we're missing the |JavaNull in the return type. - // i.e. test that the nullability is propagated to nested containers. - val ll = new ArrayList[ArrayList[ArrayList[String]]] - val level1: ArrayList[ArrayList[String]] = ll.get(0) // error - val level2: ArrayList[String] = ll.get(0).get(0) // error - val level3: String = ll.get(0).get(0).get(0) // error - val ok: String = ll.get(0).get(0).get(0) // error - - // Test that return values in PolyTypes are marked as nullable. - val lstring = new ArrayList[String]() - val res: String = java.util.Collections.max(lstring) // error: missing |Null - val res2: String|Null = java.util.Collections.max(lstring) // ok - } -} diff --git a/tests/neg/explicit-null-default.scala b/tests/neg/explicit-null-default.scala new file mode 100644 index 000000000000..fddc9dd98af4 --- /dev/null +++ b/tests/neg/explicit-null-default.scala @@ -0,0 +1,17 @@ + +class Foo { + val x: String = null // error: String is non-nullable + + def foo(x: String): String = "x" + + val y = foo(null) // error: String argument is non-nullable + + val z = foo("hello") + + if (z == null) { // error: z is non-nullable + } + + class Bar + + val b: Bar = null // error: user-created classes are also non-nullable +} diff --git a/tests/neg/explicit-null-interop-javanull.scala b/tests/neg/explicit-null-interop-javanull.scala new file mode 100644 index 000000000000..1a1924016491 --- /dev/null +++ b/tests/neg/explicit-null-interop-javanull.scala @@ -0,0 +1,8 @@ + +// Test that JavaNull can be assigned to Null. +class Foo { + import java.util.ArrayList + val l = new ArrayList[String]() + val s: String = l.get(0) // error: return type is nullable + val s2: String|Null = l.get(0) // ok +} diff --git a/tests/neg/explicit-null-interop-polytypes.scala b/tests/neg/explicit-null-interop-polytypes.scala new file mode 100644 index 000000000000..5718e0fc564d --- /dev/null +++ b/tests/neg/explicit-null-interop-polytypes.scala @@ -0,0 +1,7 @@ +class Foo { + import java.util.ArrayList + // Test that return values in PolyTypes are marked as nullable. + val lstring = new ArrayList[String]() + val res: String = java.util.Collections.max(lstring) // error: missing |Null + val res2: String|Null = java.util.Collections.max(lstring) // ok +} diff --git a/tests/neg/explicit-null-interop-propagate.scala b/tests/neg/explicit-null-interop-propagate.scala new file mode 100644 index 000000000000..c21728fb7395 --- /dev/null +++ b/tests/neg/explicit-null-interop-propagate.scala @@ -0,0 +1,11 @@ + class Foo { + import java.util.ArrayList + + // Test that as we extract return values, we're missing the |JavaNull in the return type. + // i.e. test that the nullability is propagated to nested containers. + val ll = new ArrayList[ArrayList[ArrayList[String]]] + val level1: ArrayList[ArrayList[String]] = ll.get(0) // error + val level2: ArrayList[String] = ll.get(0).get(0) // error + val level3: String = ll.get(0).get(0).get(0) // error + val ok: String = ll.get(0).get(0).get(0) // error +} diff --git a/tests/neg/explicit-null-interop-return.scala b/tests/neg/explicit-null-interop-return.scala new file mode 100644 index 000000000000..677d9528e6fa --- /dev/null +++ b/tests/neg/explicit-null-interop-return.scala @@ -0,0 +1,14 @@ + +// Test that the return type of Java methods as well as the type of Java fields is marked as nullable. +class Foo { + + def foo = { + import java.util.ArrayList + val x = new ArrayList[String]() + val r: String = x.get(0) // error: got String|JavaNull instead of String + + val x2 = new ArrayList[Int]() + val r2: Int = x2.get(0) // error: even though Int is non-nullable in Scala, its counterpart + // (for purposes of generics) in Java (Integer) is. So we're missing |JavaNull + } +} diff --git a/tests/pos-java-interop/explicit-null-classfile.scala b/tests/pos-java-interop/explicit-null-classfile.scala deleted file mode 100644 index fc4b4d1d06f0..000000000000 --- a/tests/pos-java-interop/explicit-null-classfile.scala +++ /dev/null @@ -1,20 +0,0 @@ - -// Tests that we add "|JavaNull" to Java fields and methods from classfiles. -class Foo { - - def foo = { - import java.util.ArrayList - import java.util.Iterator - val x = new ArrayList[String]() - x.add(null) // |JavaNull added to method argument - // TODO(abeln): we can't directly test that |JavaNull was added to the return type - // in a positive test. We test it in the negative counterpart instead. - - // Test that we can select through "|JavaNull" (unsoundly). - val x3 = new ArrayList[ArrayList[ArrayList[String]]]() - val x4: Int = x3.get(0).get(0).get(0).length() - - // Test that generic methods are also tagged as nullable. - java.util.Collections.max(null) - } -} diff --git a/tests/pos-java-interop/explicit-null-source/ArrayList.java b/tests/pos-java-interop/explicit-null-source/ArrayList.java deleted file mode 100644 index 9b29e4401078..000000000000 --- a/tests/pos-java-interop/explicit-null-source/ArrayList.java +++ /dev/null @@ -1,6 +0,0 @@ -class ArrayList { - ArrayList() {} - void add(T x) {} - T get(int x) { return null; } - Iterator iterator() { return new Iterator(); } -} diff --git a/tests/pos-java-interop/explicit-null-source/Iterator.java b/tests/pos-java-interop/explicit-null-source/Iterator.java deleted file mode 100644 index 2afb13e586ae..000000000000 --- a/tests/pos-java-interop/explicit-null-source/Iterator.java +++ /dev/null @@ -1,3 +0,0 @@ -class Iterator { - Iterator() {} -} diff --git a/tests/pos-java-interop/explicit-null-source/source.scala b/tests/pos-java-interop/explicit-null-source/source.scala deleted file mode 100644 index 81be19f9b8e5..000000000000 --- a/tests/pos-java-interop/explicit-null-source/source.scala +++ /dev/null @@ -1,15 +0,0 @@ - -// Tests that we add "|JavaNull" to Java fields and methods from classfiles. -class Foo { - - def foo = { - val x = new ArrayList[String]() - x.add(null) // |JavaNull added to method argument - // TODO(abeln): we can't directly test that |JavaNull was added to the return type - // in a positive test. We test it in the negative counterpart instead. - - // Test that we can select through "|JavaNull" (unsoundly). - val x3 = new ArrayList[ArrayList[ArrayList[String]]]() - val x4: Int = x3.get(0).get(0).get(0).length() - } -} diff --git a/tests/pos/explicit-null-interop-constructor-src/J.java b/tests/pos/explicit-null-interop-constructor-src/J.java new file mode 100644 index 000000000000..209f8067b42d --- /dev/null +++ b/tests/pos/explicit-null-interop-constructor-src/J.java @@ -0,0 +1,5 @@ +class J { + private String s; + + J(String x) { this.s = x; } +} diff --git a/tests/pos/explicit-null-interop-constructor-src/S.scala b/tests/pos/explicit-null-interop-constructor-src/S.scala new file mode 100644 index 000000000000..abf584e2cdca --- /dev/null +++ b/tests/pos/explicit-null-interop-constructor-src/S.scala @@ -0,0 +1,5 @@ + +class S { + val x: J = new J("hello") + val x2: J = new J(null) +} diff --git a/tests/pos/explicit-null-interop-constructor.scala b/tests/pos/explicit-null-interop-constructor.scala new file mode 100644 index 000000000000..3d5a05428a45 --- /dev/null +++ b/tests/pos/explicit-null-interop-constructor.scala @@ -0,0 +1,6 @@ + +// Test that constructors have a non-nullab.e return type. +class Foo { + val x: java.lang.String = new java.lang.String() + val y: java.util.Date = new java.util.Date() +} diff --git a/tests/pos/explicit-null-interop-javanull-src/J.java b/tests/pos/explicit-null-interop-javanull-src/J.java new file mode 100644 index 000000000000..a85afa17c859 --- /dev/null +++ b/tests/pos/explicit-null-interop-javanull-src/J.java @@ -0,0 +1,8 @@ + +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} diff --git a/tests/pos/explicit-null-interop-javanull-src/S.scala b/tests/pos/explicit-null-interop-javanull-src/S.scala new file mode 100644 index 000000000000..0f5c51a18ccc --- /dev/null +++ b/tests/pos/explicit-null-interop-javanull-src/S.scala @@ -0,0 +1,6 @@ + +// Test that JavaNull is "see through" +class S { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() +} diff --git a/tests/pos/explicit-null-interop-javanull.scala b/tests/pos/explicit-null-interop-javanull.scala new file mode 100644 index 000000000000..636475166cbf --- /dev/null +++ b/tests/pos/explicit-null-interop-javanull.scala @@ -0,0 +1,10 @@ + +// Tests that the "JavaNull" type added to Java types is "see through" w.r.t member selections. +class Foo { + import java.util.ArrayList + import java.util.Iterator + + // Test that we can select through "|JavaNull" (unsoundly). + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() +} diff --git a/tests/pos/explicit-null-interop-static/J.java b/tests/pos/explicit-null-interop-static/J.java new file mode 100644 index 000000000000..94734378b69c --- /dev/null +++ b/tests/pos/explicit-null-interop-static/J.java @@ -0,0 +1,4 @@ + +class J { + static int foo(S s) { return 42; } +} diff --git a/tests/pos/explicit-null-interop-static/S.scala b/tests/pos/explicit-null-interop-static/S.scala new file mode 100644 index 000000000000..e54a33cd175b --- /dev/null +++ b/tests/pos/explicit-null-interop-static/S.scala @@ -0,0 +1,6 @@ + +class S { + + J.foo(null) // Java static methods are also nullified + +} diff --git a/tests/pos/explicit-null-interop-valuetypes.scala b/tests/pos/explicit-null-interop-valuetypes.scala new file mode 100644 index 000000000000..595a7de8917a --- /dev/null +++ b/tests/pos/explicit-null-interop-valuetypes.scala @@ -0,0 +1,6 @@ + +// Tests that value (non-reference) types aren't nullified by the Java transform. +class Foo { + val x: java.lang.String = "" + val len: Int = x.length() // type is Int and not Int|JavaNull +} diff --git a/tests/pos/explicit-null-throw-null.scala b/tests/pos/explicit-null-throw-null.scala new file mode 100644 index 000000000000..8abaecdaf5f0 --- /dev/null +++ b/tests/pos/explicit-null-throw-null.scala @@ -0,0 +1,4 @@ + +class Foo { + throw null // Still typecheks: throws NPE +} diff --git a/tests/pos/explicit-null-union.scala b/tests/pos/explicit-null-union.scala new file mode 100644 index 000000000000..7c1d232af684 --- /dev/null +++ b/tests/pos/explicit-null-union.scala @@ -0,0 +1,4 @@ + +class Foo { + val x: String|Null = null // nullable types can be represented with unions +} From 3f8d9495e0580e0b61c82aa2c43460856303ea1d Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Sep 2018 12:28:33 -0400 Subject: [PATCH 022/127] Handle constructors properly in nullability transform This adds a second TypeMap, specifically for constructors. For constructors, we nullify the argument types, but not the return type. Once we nullify the arguments of constructors, all case classes were breaking, because the logic to generate synthetic methods for case classes relies on finding the symbol for IndexOutOfBoundException, which changed with this CL. Patch up that logic as well. --- .../dotty/tools/dotc/core/Definitions.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 39 +++++++++++++++++-- .../dotc/core/classfile/ClassfileParser.scala | 11 ++++-- .../dotc/transform/SyntheticMethods.scala | 2 +- .../src/dotty/tools/dotc/typer/Namer.scala | 13 ++++--- .../J.java | 1 + .../S.scala | 1 + .../explicit-null-interop-constructor.scala | 1 + 8 files changed, 56 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 15fedcef07a7..d8f86855bc08 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -349,7 +349,7 @@ class Definitions { lazy val JavaNull = enterAliasType(tpnme.JavaNull, AnnotatedType.make(NullType, List(Annotation(JavaNullAnnot)))) def JavaNullType = JavaNull.typeRef - def javaNullable(tp: Type) = if (tp.isJavaNullable) tp else OrType(tp, JavaNullType) + def javaNullable(tp: Type) = OrType(tp, JavaNullType) lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 955458a421b0..61088a05c660 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -994,7 +994,7 @@ object Types { /** Converts types of the form `JavaNull | T` to `T | JavaNull`. Does not do any widening or dealiasing. */ def normalizeJavaNull(implicit ctx: Context): Type = { this match { - case OrType(left, right) if left.isJavaNull => OrType(right, left) + case OrType(left, right) if left.isJavaNull => defn.javaNullable(right) case _ => this } } @@ -4764,6 +4764,35 @@ object Types { protected def reapply(tp: Type): Type = apply(tp) } + /** Adds nullability annotations to a Java-defined member. + * `tp` is the member type. The type inside `sym` shouldn't be used (might not be even set). + */ + def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { + assert(sym.is(JavaDefined), s"can only nullify java-defined members") + if (sym.isConstructor) { + val constrMap = new ConstructorNullMap() + constrMap(tp) + } else { + val nullMap = new JavaNullMap() + nullMap(tp) + } + } + + /** Adds nullability annotations to the arguments of a constructor, but not to the return type. */ + class ConstructorNullMap(implicit ctx: Context) extends TypeMap { + override def apply(tp: Type): Type = { + tp match { + case tp: MethodType => + val nullMap = new JavaNullMap() + derivedLambdaType(tp)(tp.paramInfos.mapConserve(nullMap), tp.resType) + case tp: PolyType => + mapOver(tp) + case _ => + tp + } + } + } + /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact * that Java types remain nullable by default. * @@ -4772,8 +4801,10 @@ object Types { */ class JavaNullMap(implicit ctx: Context) extends TypeMap { def shouldNullify(tp: TypeRef): Boolean = { - val res = !tp.symbol.isValueClass && !tp.symbol.derivesFrom(defn.AnnotationClass) - res + !tp.symbol.isValueClass && + !tp.symbol.derivesFrom(defn.AnnotationClass) && + !tp.isRef(defn.ObjectClass) && + !tp.isRef(defn.AnyClass) } override def apply(tp: Type): Type = { @@ -4786,6 +4817,8 @@ object Types { defn.javaNullable(tp) case tp@RefinedType(parent, name, info) => // TODO(abeln): does this ever happen for Java-sourced types? defn.javaNullable(derivedRefinedType(tp, parent, this(info))) + case tp: TypeParamRef => + defn.javaNullable(tp) case AppliedType(tycons, targs) => defn.javaNullable(AppliedType(tycons, targs map this)) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index b8cdee2e5741..90c2c4bdfb4f 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -7,6 +7,7 @@ import Contexts._, Symbols._, Types._, Names._, StdNames._, NameOps._, Scopes._, import SymDenotations._, unpickleScala2.Scala2Unpickler._, Constants._, Annotations._, util.Positions._ import NameKinds.DefaultGetterName import dotty.tools.dotc.core.tasty.{TastyHeaderUnpickler, TastyReader} +import dotty.tools.dotc.config.Printers import ast.tpd._ import java.io.{ ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, IOException } @@ -263,17 +264,21 @@ class ClassfileParser( addConstructorTypeParams(denot) } + def nullifyTpe(): Unit = { + val old = denot.info + denot.info = nullifyMember(denot.symbol, denot.info) + Printers.nullability.println(s"nullified member type from classfile for ${denot.symbol.name.show} from ${old.show} into ${denot.info.show}") + } + denot.info = pool.getType(in.nextChar) if (isEnum) denot.info = ConstantType(Constant(sym)) if (isConstructor) normalizeConstructorParams() setPrivateWithin(denot, jflags) denot.info = translateTempPoly(parseAttributes(sym, denot.info)) if (isConstructor) normalizeConstructorInfo() - val nullMap = new JavaNullMap - if (!isConstructor) denot.info = nullMap(denot.info) if ((denot is Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0) denot.info = arrayToRepeated(denot.info) - + nullifyTpe() // seal java enums if (isEnum) { val enumClass = sym.owner.linkedClass diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMethods.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMethods.scala index 0b9dde117732..413bf0c3d7e4 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMethods.scala @@ -117,7 +117,7 @@ class SyntheticMethods(thisPhase: DenotTransformer) { val ioob = defn.IndexOutOfBoundsException.typeRef // Second constructor of ioob that takes a String argument def filterStringConstructor(s: Symbol): Boolean = s.info match { - case m: MethodType if s.isConstructor => m.paramInfos == List(defn.StringType) + case m: MethodType if s.isConstructor && m.paramInfos.size == 1 => m.paramInfos.head.stripJavaNull == defn.StringType case _ => false } val constructor = ioob.typeSymbol.info.decls.find(filterStringConstructor _).asTerm diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index ad48c9369172..804ca2cbd678 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -19,6 +19,7 @@ import Annotations._ import Inferencing._ import transform.ValueClasses._ import reporting.diagnostic.messages._ +import dotty.tools.dotc.config.Printers trait NamerContextOps { this: Context => import NamerContextOps._ @@ -1176,13 +1177,13 @@ class Namer { typer: Typer => case _ => WildcardType } - val resTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) - val isConstructor = mdef.name == nme.CONSTRUCTOR - if (mdef.mods.is(JavaDefined) && !isConstructor) { - val nullMap = new JavaNullMap - nullMap(resTpe) + val memTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + if (mdef.mods.is(JavaDefined)) { + val nullifiedTpe = nullifyMember(sym, memTpe) + Printers.nullability.println(s"nullified member type from source for ${sym.name.show} from ${memTpe.show} into ${nullifiedTpe.show}") + nullifiedTpe } else { - resTpe + memTpe } } diff --git a/tests/pos/explicit-null-interop-constructor-src/J.java b/tests/pos/explicit-null-interop-constructor-src/J.java index 209f8067b42d..b1590d50023e 100644 --- a/tests/pos/explicit-null-interop-constructor-src/J.java +++ b/tests/pos/explicit-null-interop-constructor-src/J.java @@ -2,4 +2,5 @@ class J { private String s; J(String x) { this.s = x; } + J(String x, String y, String z) {} } diff --git a/tests/pos/explicit-null-interop-constructor-src/S.scala b/tests/pos/explicit-null-interop-constructor-src/S.scala index abf584e2cdca..6cbfea9b57b1 100644 --- a/tests/pos/explicit-null-interop-constructor-src/S.scala +++ b/tests/pos/explicit-null-interop-constructor-src/S.scala @@ -2,4 +2,5 @@ class S { val x: J = new J("hello") val x2: J = new J(null) + val x3: J = new J(null, null, null) } diff --git a/tests/pos/explicit-null-interop-constructor.scala b/tests/pos/explicit-null-interop-constructor.scala index 3d5a05428a45..1f631e6efff6 100644 --- a/tests/pos/explicit-null-interop-constructor.scala +++ b/tests/pos/explicit-null-interop-constructor.scala @@ -3,4 +3,5 @@ class Foo { val x: java.lang.String = new java.lang.String() val y: java.util.Date = new java.util.Date() + val v = new java.util.Vector[String](null /*stands for Collection*/) } From b048428dc6e5e8bbcadf0c377f881b4e0543d392 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Sep 2018 16:42:04 -0400 Subject: [PATCH 023/127] Better check for whether we're before erasure --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d8f86855bc08..0ad17c370635 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -973,12 +973,12 @@ class Definitions { def isBottomClass(cls: Symbol) = { // After erasure, reference types become nullable again. - if (ctx.phaseId <= ctx.erasurePhase.id) cls == NothingClass + if (!ctx.phase.erasedTypes) cls == NothingClass else cls == NothingClass || cls == NullClass } def isBottomType(tp: Type) = { // After erasure, reference types become nullable again. - if (ctx.phaseId <= ctx.erasurePhase.id) tp.derivesFrom(NothingClass) + if (!ctx.phase.erasedTypes) tp.derivesFrom(NothingClass) else tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass) } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 1d045ae2e750..f9fe6e984d06 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -683,7 +683,7 @@ object SymDenotations { /** Is this symbol a class with nullable values? */ final def isNullableClass(implicit ctx: Context): Boolean = { // After erasure, reference types become nullable again. - if (ctx.phaseId <= ctx.erasurePhase.id) symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass + if (!ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass else isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass } From 9491c16ba75f4f53d4c23f784e00a6db1cae638b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Sep 2018 16:45:44 -0400 Subject: [PATCH 024/127] Add test for nullifying methods --- tests/neg/explicit-null-interop-method-src/J.java | 5 +++++ tests/neg/explicit-null-interop-method-src/S.scala | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/neg/explicit-null-interop-method-src/J.java create mode 100644 tests/neg/explicit-null-interop-method-src/S.scala diff --git a/tests/neg/explicit-null-interop-method-src/J.java b/tests/neg/explicit-null-interop-method-src/J.java new file mode 100644 index 000000000000..1b7ea514e4b2 --- /dev/null +++ b/tests/neg/explicit-null-interop-method-src/J.java @@ -0,0 +1,5 @@ + +class J { + String foo(String x) { return null; } + static String fooStatic(String x) { return null; } +} diff --git a/tests/neg/explicit-null-interop-method-src/S.scala b/tests/neg/explicit-null-interop-method-src/S.scala new file mode 100644 index 000000000000..403c86bc4c06 --- /dev/null +++ b/tests/neg/explicit-null-interop-method-src/S.scala @@ -0,0 +1,10 @@ + +class S { + + val j = new J() + j.foo(null) // ok: argument is nullable + val s: String = j.foo("hello") // error: return type is nullable + + J.fooStatic(null) // ok: argument is nullable + val s2: String = J.fooStatic("hello") // error: return type is nullable +} From 270960ce70ca9ced831f50c0a65bcde13e5306f5 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Sep 2018 17:36:41 -0400 Subject: [PATCH 025/127] Don't nullify the inside of Class[] and the special TYPE field --- .../src/dotty/tools/dotc/core/Types.scala | 33 +++++++++++++++---- tests/pos/explicit-null-new-array.scala | 5 +++ tests/pos/explicit-null-type-field.scala | 5 +++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 tests/pos/explicit-null-new-array.scala create mode 100644 tests/pos/explicit-null-type-field.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 61088a05c660..6f48f610ef15 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4770,8 +4770,13 @@ object Types { def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { assert(sym.is(JavaDefined), s"can only nullify java-defined members") if (sym.isConstructor) { + // Constructors get special treatment because the return type isn't nullified. val constrMap = new ConstructorNullMap() constrMap(tp) + } else if (sym.name == nme.TYPE_) { + // The special TYPE field returns the Class object for a given class, so it's + // always non-null. + tp } else { val nullMap = new JavaNullMap() nullMap(tp) @@ -4800,11 +4805,24 @@ object Types { * nullify(C[S]) = C[nullify(S)] | JavaNull if C is a generic class */ class JavaNullMap(implicit ctx: Context) extends TypeMap { - def shouldNullify(tp: TypeRef): Boolean = { - !tp.symbol.isValueClass && - !tp.symbol.derivesFrom(defn.AnnotationClass) && - !tp.isRef(defn.ObjectClass) && - !tp.isRef(defn.AnyClass) + def shouldNullify(tp: Type): Boolean = { + tp match { + case tp: TypeRef => + !tp.symbol.isValueClass && + !tp.symbol.derivesFrom(defn.AnnotationClass) && + !tp.isRef(defn.ObjectClass) && + !tp.isRef(defn.AnyClass) + case _ => + true + } + } + + def shouldDescend(tp: AppliedType): Boolean = { + val AppliedType(tycons, _) = tp + // Since `Class` objects are runtime representations of _classes_, it doesn't make + // sense to talk about e.g. Class[String | JavaNull], so we don't recursive inside `Class` + // while nullifying things. + tycons != defn.ClassClass.typeRef } override def apply(tp: Type): Type = { @@ -4819,8 +4837,9 @@ object Types { defn.javaNullable(derivedRefinedType(tp, parent, this(info))) case tp: TypeParamRef => defn.javaNullable(tp) - case AppliedType(tycons, targs) => - defn.javaNullable(AppliedType(tycons, targs map this)) + case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => + val targs2 = if (shouldDescend(appTp)) targs map this else targs + defn.javaNullable(AppliedType(tycons, targs2)) case _ => tp } diff --git a/tests/pos/explicit-null-new-array.scala b/tests/pos/explicit-null-new-array.scala new file mode 100644 index 000000000000..a15db12f3340 --- /dev/null +++ b/tests/pos/explicit-null-new-array.scala @@ -0,0 +1,5 @@ + +class S { + val x = new Array[Int](10) + val y = new Array[String](10) +} diff --git a/tests/pos/explicit-null-type-field.scala b/tests/pos/explicit-null-type-field.scala new file mode 100644 index 000000000000..69c5fadef819 --- /dev/null +++ b/tests/pos/explicit-null-type-field.scala @@ -0,0 +1,5 @@ + +class S { + // verify that the special TYPE field is non-nullable + val x: Class[Integer] = java.lang.Integer.TYPE +} From 2b4a7d6c03680c671a62e8608a0803d9dc288f84 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 14 Sep 2018 17:54:27 -0400 Subject: [PATCH 026/127] Fix a couple of failing tests --- tests/pos/t0032.scala | 2 +- tests/pos/t0872.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/t0032.scala b/tests/pos/t0032.scala index 727a7d4e992d..a8724dfa19b7 100644 --- a/tests/pos/t0032.scala +++ b/tests/pos/t0032.scala @@ -1,6 +1,6 @@ import java.io.{OutputStream, PrintStream}; -class PromptStream(s: OutputStream) extends PrintStream(s) { +class PromptStream(s: OutputStream|Null) extends PrintStream(s) { override def println() = super.println(); } diff --git a/tests/pos/t0872.scala b/tests/pos/t0872.scala index ccaee80521c2..dcae0d338058 100644 --- a/tests/pos/t0872.scala +++ b/tests/pos/t0872.scala @@ -1,7 +1,7 @@ object Main { def main(args : Array[String]): Unit = { val fn = (a : Int, str : String) => "a: " + a + ", str: " + str - implicit def fx[T](f : (T,String) => String): T => String = (x:T) => f(x,null) + implicit def fx[T](f : (T,String) => String): T => String = (x:T) => f(x,???) println(fn(1)) () } From 9a86c341b683c53097d26f1752f5a9d66d2d5832 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Sep 2018 17:39:44 -0400 Subject: [PATCH 027/127] Fix two more tests --- tests/pos/i3542-2.scala | 6 +++--- tests/pos/t8177a.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pos/i3542-2.scala b/tests/pos/i3542-2.scala index 90ece1ea2d78..3ad608dd149e 100644 --- a/tests/pos/i3542-2.scala +++ b/tests/pos/i3542-2.scala @@ -3,16 +3,16 @@ trait ::[H, T] trait Foo[A, R] trait FooLowPrio { - implicit def caseOther[A]: Foo[A, A :: Any] = null + implicit def caseOther[A]: Foo[A, A :: Any] = ??? } object Foo extends FooLowPrio { implicit def caseCons[H, HR, T, TR] (implicit // It's a bit artificial: the by name is not required in this example... t: => Foo[T, TR], h: => Foo[H, HR] - ): Foo[H :: T, TR] = null + ): Foo[H :: T, TR] = ??? - implicit def caseAny: Foo[Any, Any] = null + implicit def caseAny: Foo[Any, Any] = ??? } object Test { diff --git a/tests/pos/t8177a.scala b/tests/pos/t8177a.scala index 7e2cfb386c3e..4b42ad5551c2 100644 --- a/tests/pos/t8177a.scala +++ b/tests/pos/t8177a.scala @@ -4,6 +4,6 @@ class AA[T](final val x: Thing { type A = T }) { def foo: x.A = ??? } -class B extends AA[Int](null) { +class B extends AA[Int](???) { override def foo: B.this.x.A = super.foo } From 9f4a6d4e5d0138b41257bae3ee16b29acf3bdf19 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Sep 2018 17:40:41 -0400 Subject: [PATCH 028/127] Handle repeated parameters --- .../tools/dotc/core/TypeApplications.scala | 4 +++- compiler/src/dotty/tools/dotc/core/Types.scala | 17 ++++++++++++++--- .../dotty/tools/dotc/typer/Applications.scala | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index c38b777a3b20..5f861cf85ae7 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -456,10 +456,12 @@ class TypeApplications(val self: Type) extends AnyVal { */ def underlyingIfRepeated(isJava: Boolean)(implicit ctx: Context): Type = if (self.isRepeatedParam) { + val self1 = self.stripJavaNull val seqClass = if (isJava) defn.ArrayClass else defn.SeqClass // If `isJava` is set, then we want to turn `RepeatedParam[T]` into `Array[_ <: T]`, // since arrays aren't covariant until after erasure. See `tests/pos/i5140`. - translateParameterized(defn.RepeatedParamClass, seqClass, wildcardArg = isJava) + val trans = self1.translateParameterized(defn.RepeatedParamClass, seqClass, wildcardArg = isJava) + if (isJava) defn.javaNullable(trans) else trans } else self diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 6f48f610ef15..926dc2e2b6bc 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -321,8 +321,12 @@ object Types { (new isGroundAccumulator).apply(true, this) /** Is this a type of a repeated parameter? */ - def isRepeatedParam(implicit ctx: Context): Boolean = - typeSymbol eq defn.RepeatedParamClass + def isRepeatedParam(implicit ctx: Context): Boolean = { + def isRep(tpe: Type) = tpe.typeSymbol eq defn.RepeatedParamClass + // A repeated param is represented as either RepeatedParamClass[ElemTpe] or + // RepeatedParamClass[ElemTpe]|JavaNull, if it comes from Java. + isRep(this) || isRep(stripJavaNull) + } /** Is this the type of a method that has a repeated parameter type as * last parameter type? @@ -1231,7 +1235,14 @@ object Types { /** If this is a repeated type, its element type, otherwise the type itself */ def repeatedToSingle(implicit ctx: Context): Type = this match { case tp @ ExprType(tp1) => tp.derivedExprType(tp1.repeatedToSingle) - case _ => if (isRepeatedParam) this.argTypesHi.head else this + case _ => + if (isRepeatedParam) { + val repTpe = stripJavaNull + assert(repTpe.argTypesLo.size == 1, s"Found repeated parameter type with more than one argument: ${this.show}") + repTpe.argTypesLo.head + } else { + this + } } /** If this is a FunProto or PolyProto, WildcardType, otherwise this. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 438249f89124..b5bb5610b387 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -531,7 +531,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => case arg :: Nil if isVarArg(arg) => addTyped(arg, formal) case _ => - val elemFormal = formal.widenExpr.argTypesLo.head + val elemFormal = formal.repeatedToSingle val typedArgs = harmonic(harmonizeArgs, elemFormal)(args.map(typedArg(_, elemFormal))) typedArgs.foreach(addArg(_, elemFormal)) From c1df4c7010eecf2a8e50d4d1a498d6dc3a90ea57 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Sep 2018 18:04:29 -0400 Subject: [PATCH 029/127] Fix another test --- tests/pos/Transactions.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/pos/Transactions.scala b/tests/pos/Transactions.scala index 1c483a666cfd..dd5b9482fbea 100644 --- a/tests/pos/Transactions.scala +++ b/tests/pos/Transactions.scala @@ -25,7 +25,7 @@ class Transaction { var id: Long = _ // only for real transactions var head: Transaction = this - var next: Transaction = ??? + var next: Transaction|Null = null def this(hd: Transaction, tl: Transaction) = { this(); this.head = head; this.next = next } @@ -52,6 +52,8 @@ class Transaction { } trait Transactional { + // TODO(abeln): replace by stdlib function. + implicit def denullify[T](x: T|Null): T = x.asInstanceOf[T] /** create a new snapshot */ def checkPoint(): Unit @@ -60,9 +62,9 @@ trait Transactional { def rollBack(): Unit var readers: Transaction - var writer: Transaction + var writer: Transaction|Null - def currentWriter(): Transaction = ??? + def currentWriter(): Transaction|Null = null if (writer == null) null else if (writer.status == Transaction.Running) writer else { @@ -83,7 +85,7 @@ trait Transactional { } synchronized { if (thisTrans.status == Transaction.Abortable) throw new AbortException - val w = currentWriter() + val w: Transaction|Null = currentWriter() if (w != null) if (thisTrans.id < w.id) { w.makeAbort(); rollBack(); writer = null } else throw new AbortException @@ -94,7 +96,8 @@ trait Transactional { def setter(thisTrans: Transaction): Unit = { if (writer == thisTrans) return synchronized { - val w = currentWriter() + // TODO(abeln): remove type annotation once type inference works. + val w: Transaction|Null = currentWriter() if (w != null) if (thisTrans.id < w.id) { w.makeAbort(); rollBack() } else throw new AbortException From 02cefbc7236950c141f7937ebb2d9d726da129dc Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Sep 2018 18:21:10 -0400 Subject: [PATCH 030/127] Don't nullify the return type of toString --- compiler/src/dotty/tools/dotc/core/Types.scala | 9 ++++++--- tests/pos/explicit-null-tostring.scala | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 tests/pos/explicit-null-tostring.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 926dc2e2b6bc..33c934901317 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4780,13 +4780,16 @@ object Types { */ def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { assert(sym.is(JavaDefined), s"can only nullify java-defined members") + // A list of members that aren't nullified. + val whitelist = Seq( + nme.TYPE_, // the special TYPE field returns the Class object for a given class, so it's always non-null. + nme.toString_ // toString has a non-nullable return type that is later dynamically checked + ) if (sym.isConstructor) { // Constructors get special treatment because the return type isn't nullified. val constrMap = new ConstructorNullMap() constrMap(tp) - } else if (sym.name == nme.TYPE_) { - // The special TYPE field returns the Class object for a given class, so it's - // always non-null. + } else if (whitelist.contains(sym.name)) { tp } else { val nullMap = new JavaNullMap() diff --git a/tests/pos/explicit-null-tostring.scala b/tests/pos/explicit-null-tostring.scala new file mode 100644 index 000000000000..6d4798badfa2 --- /dev/null +++ b/tests/pos/explicit-null-tostring.scala @@ -0,0 +1,8 @@ + +// Check that the return type of toString() isn't nullable. +class Foo { + + val x: java.lang.Integer = 42 + val s: String = x.toString() + +} From 09ea9a1456739b61c6f1d51a9c9fe75330c36d9b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 12:13:45 -0400 Subject: [PATCH 031/127] Add two explicit null tests --- tests/neg/explicit-null-array-src/J.java | 3 +++ tests/neg/explicit-null-array-src/S.scala | 10 ++++++++++ tests/neg/explicit-null-varargs-src/J.java | 12 ++++++++++++ tests/neg/explicit-null-varargs-src/S.scala | 14 ++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 tests/neg/explicit-null-array-src/J.java create mode 100644 tests/neg/explicit-null-array-src/S.scala create mode 100644 tests/neg/explicit-null-varargs-src/J.java create mode 100644 tests/neg/explicit-null-varargs-src/S.scala diff --git a/tests/neg/explicit-null-array-src/J.java b/tests/neg/explicit-null-array-src/J.java new file mode 100644 index 000000000000..80fda83e89d7 --- /dev/null +++ b/tests/neg/explicit-null-array-src/J.java @@ -0,0 +1,3 @@ +class J { + void foo(String[] ss) {} +} diff --git a/tests/neg/explicit-null-array-src/S.scala b/tests/neg/explicit-null-array-src/S.scala new file mode 100644 index 000000000000..3796bab79970 --- /dev/null +++ b/tests/neg/explicit-null-array-src/S.scala @@ -0,0 +1,10 @@ +class S { + + val j = new J() + val x: Array[String] = ??? + j.foo(x) // error: expected Array[String|Null] but got Array[String] + + val x2: Array[String|Null] = ??? + j.foo(x2) // ok + j.foo(null) // ok +} diff --git a/tests/neg/explicit-null-varargs-src/J.java b/tests/neg/explicit-null-varargs-src/J.java new file mode 100644 index 000000000000..268b7f065ddf --- /dev/null +++ b/tests/neg/explicit-null-varargs-src/J.java @@ -0,0 +1,12 @@ +class J { + void foo(String... ss) {} +} + +class Animal {} + +class Dog extends Animal {} + +class J2 { + void foo(Animal... animal) {} +} + diff --git a/tests/neg/explicit-null-varargs-src/S.scala b/tests/neg/explicit-null-varargs-src/S.scala new file mode 100644 index 000000000000..edea919e2ca7 --- /dev/null +++ b/tests/neg/explicit-null-varargs-src/S.scala @@ -0,0 +1,14 @@ +class S { + + val j2 = new J2() + val x: Array[Dog] = ??? + j2.foo(x: _*) + + //val j = new J() + //val x: Array[String] = ??? + //j.foo(x: _*) // error: expected Array[String|Null] but got Array[String] + + //val x2: Array[String|Null] = ??? + //j.foo(x2: _*) // ok + // j.foo(null: _*) // ok +} From ed13029367e06ba49095d4c041766efad02c2391 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 14:16:30 -0400 Subject: [PATCH 032/127] Fix tests/pos/annotations2.scala --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/Types.scala | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index bba5b97ad21a..fa3726ba6629 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -469,6 +469,7 @@ object StdNames { val name: N = "name" val ne: N = "ne" val newFreeTerm: N = "newFreeTerm" + val newInstance: N = "newInstance" val newFreeType: N = "newFreeType" val newScopeWith: N = "newScopeWith" val next: N = "next" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 33c934901317..f5b73d77b2f8 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4781,15 +4781,26 @@ object Types { def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { assert(sym.is(JavaDefined), s"can only nullify java-defined members") // A list of members that aren't nullified. - val whitelist = Seq( - nme.TYPE_, // the special TYPE field returns the Class object for a given class, so it's always non-null. - nme.toString_ // toString has a non-nullable return type that is later dynamically checked + val whitelist: Seq[Symbol => Boolean] = Seq( + // The special TYPE field returns the Class object for a given class, so it's always non-null. + _.name == nme.TYPE_, + // toString has a non-nullable return type that is later dynamically checked + _.name == nme.toString_ , + // newInstance() method in class `Class` + (s: Symbol) => + s.owner == defn.ClassClass && s.name == nme.newInstance ) + // Is `sym` whitelisted (so that it's not nullified) + val isWhitelisted = whitelist.foldLeft(false) { + case (wl, pred) => + if (wl) true + else pred(sym) + } if (sym.isConstructor) { // Constructors get special treatment because the return type isn't nullified. val constrMap = new ConstructorNullMap() constrMap(tp) - } else if (whitelist.contains(sym.name)) { + } else if (isWhitelisted) { tp } else { val nullMap = new JavaNullMap() From 4c4cce78ae005fff91e8b3d5e0188df2b50a8699 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 14:18:28 -0400 Subject: [PATCH 033/127] Fix tests/pos/tcpoly_seq.scala --- tests/pos/tcpoly_seq.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/tcpoly_seq.scala b/tests/pos/tcpoly_seq.scala index 312ba7405818..50a55b14654b 100644 --- a/tests/pos/tcpoly_seq.scala +++ b/tests/pos/tcpoly_seq.scala @@ -67,7 +67,7 @@ trait HOSeq { start = last } else { val last1 = last - last = new HOSeq.this.:: (x, null) // hack: ::'s tail will actually be last + last = new HOSeq.this.:: (x, ???) // hack: ::'s tail will actually be last //last1.tl = last } } From b23a3e62864fbce2f03e338c593b271efdd62797 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 16:02:52 -0400 Subject: [PATCH 034/127] Add mechanism to except fields/methods from the transform --- .../src/dotty/tools/dotc/core/StdNames.scala | 10 ++ .../src/dotty/tools/dotc/core/Types.scala | 115 ++++++++++++------ .../pos/explicit-null-string-whitelist.scala | 14 +++ 3 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 tests/pos/explicit-null-string-whitelist.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index fa3726ba6629..8c9d25c931b3 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -392,6 +392,7 @@ object StdNames { val ClassManifestFactory: N = "ClassManifestFactory" val classOf: N = "classOf" val clone_ : N = "clone" + val concat : N = "concat" val conforms_ : N = "$conforms" val copy: N = "copy" val currentMirror: N = "currentMirror" @@ -494,6 +495,9 @@ object StdNames { val reflect : N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" + val replace : N = "replace" + val replaceFirst : N = "replaceFirst" + val replaceAll : N = "replaceAll" val rootMirror : N = "rootMirror" val run: N = "run" val runOrElse: N = "runOrElse" @@ -516,10 +520,12 @@ object StdNames { val setType: N = "setType" val setTypeSignature: N = "setTypeSignature" val splice: N = "splice" + val split: N = "split" val staticClass : N = "staticClass" val staticModule : N = "staticModule" val staticPackage : N = "staticPackage" val strictEquality: N = "strictEquality" + val substring: N = "substring" val synchronized_ : N = "synchronized" val tag: N = "tag" val tail: N = "tail" @@ -528,13 +534,17 @@ object StdNames { val thisPrefix : N = "thisPrefix" val throw_ : N = "throw" val toArray: N = "toArray" + val toCharArray: N = "toCharArray" val toList: N = "toList" + val toLowerCase: N = "toLowerCase" val toObjectArray : N = "toObjectArray" val toSeq: N = "toSeq" val toString_ : N = "toString" val toTypeConstructor: N = "toTypeConstructor" + val toUpperCase: N = "toUpperCase" val tpe : N = "tpe" val tree : N = "tree" + val trim: N = "trim" val true_ : N = "true" val typedProductIterator: N = "typedProductIterator" val typeTagToManifest: N = "typeTagToManifest" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f5b73d77b2f8..aa030e6f54f6 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4780,47 +4780,88 @@ object Types { */ def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { assert(sym.is(JavaDefined), s"can only nullify java-defined members") - // A list of members that aren't nullified. - val whitelist: Seq[Symbol => Boolean] = Seq( - // The special TYPE field returns the Class object for a given class, so it's always non-null. - _.name == nme.TYPE_, - // toString has a non-nullable return type that is later dynamically checked - _.name == nme.toString_ , - // newInstance() method in class `Class` - (s: Symbol) => - s.owner == defn.ClassClass && s.name == nme.newInstance - ) - // Is `sym` whitelisted (so that it's not nullified) - val isWhitelisted = whitelist.foldLeft(false) { - case (wl, pred) => - if (wl) true - else pred(sym) - } - if (sym.isConstructor) { - // Constructors get special treatment because the return type isn't nullified. - val constrMap = new ConstructorNullMap() - constrMap(tp) - } else if (isWhitelisted) { - tp - } else { - val nullMap = new JavaNullMap() - nullMap(tp) + + /** A policy that special cases the handling of some symbol or class of symbols. */ + sealed trait NullifyPolicy { + /** Whether the policy applies to `sym`. */ + def isApplicable(sym: Symbol): Boolean + /** Nullifies `tp` according to the policy. Should call `isApplicable` first. */ + def apply(tp: Type): Type } - } - /** Adds nullability annotations to the arguments of a constructor, but not to the return type. */ - class ConstructorNullMap(implicit ctx: Context) extends TypeMap { - override def apply(tp: Type): Type = { - tp match { - case tp: MethodType => - val nullMap = new JavaNullMap() - derivedLambdaType(tp)(tp.paramInfos.mapConserve(nullMap), tp.resType) - case tp: PolyType => - mapOver(tp) - case _ => - tp + /** A policy that avoids modifying a field. */ + case class FieldP(trigger: Symbol => Boolean) extends NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = trigger(sym) + override def apply(tp: Type): Type = { + assert(!tp.isJavaMethod, s"FieldPolicy applies to method (non-field) type ${tp.show}") + tp } } + + /** A policy for handling a method or poly. Can indicate whether the argument or return types should be nullified. */ + case class MethodP(trigger: Symbol => Boolean, + nlfyParams: Boolean = false, + nlfyRes: Boolean = false) extends TypeMap with NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = trigger(sym) + + override def apply(tp: Type): Type = { + tp match { + case ptp: PolyType => + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) + case mtp: MethodType => + val nullMap = new JavaNullMap() + val paramTpes = if (nlfyParams) mtp.paramInfos.mapConserve(nullMap) else mtp.paramInfos + val resTpe = if (nlfyRes) nullMap(mtp.resType) else mtp.resType + derivedLambdaType(mtp)(paramTpes, resTpe) + } + } + } + + /** A wrapper policy that works as `inner` but additionally verifies that the symbol is contained in `owner`. */ + case class WithinSym(inner: NullifyPolicy, owner: Symbol) extends NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = sym.owner == owner && inner.isApplicable(sym) + + override def apply(tp: Type): Type = inner(tp) + } + + // A list of members that are special-cased. + val whitelist: Seq[NullifyPolicy] = Seq( + // The `TYPE` field in every class. + FieldP(_.name == nme.TYPE_), + // The `toString` method. + MethodP(_.name == nme.toString_), + // The `newInstance` method in `Class`. + WithinSym(MethodP(_.name == nme.newInstance), defn.ClassClass), + // Constructors: params are nullified, but the result type isn't. + MethodP(_.isConstructor, nlfyParams = true, nlfyRes = false) + ) ++ Seq( + // Methods in `java.lang.String`. + MethodP(_.name == nme.concat, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.replace, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.replaceFirst, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.replaceAll, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.split, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.toLowerCase, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.toUpperCase, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.trim, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.toCharArray, nlfyParams = true, nlfyRes = false), + MethodP(_.name == nme.substring, nlfyParams = true, nlfyRes = false) + ).map(WithinSym(_, defn.StringClass)) + + val (fromWhitelistTp, handled) = whitelist.foldLeft((tp, false)) { + case (res@(_, true), _) => res + case ((_, false), pol) => + if (pol.isApplicable(sym)) (pol(tp), true) + else (tp, false) + } + + if (handled) { + fromWhitelistTp + } else { + // Default case: nullify everything. + val nullMap = new JavaNullMap() + nullMap(tp) + } } /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact diff --git a/tests/pos/explicit-null-string-whitelist.scala b/tests/pos/explicit-null-string-whitelist.scala new file mode 100644 index 000000000000..256daeac7e3b --- /dev/null +++ b/tests/pos/explicit-null-string-whitelist.scala @@ -0,0 +1,14 @@ +// Test that a few commonly-used functions within `java.lang.String` have been whitelisted +// as returning non-nullable values. +class Foo { + val x1: String = "abc".concat("def") + val x2: String = "abc".replace("a", "b") + val x3: String = "abc".replace("a", "b") + val x4: String = "abc".replace("a", "b") + val x5: Array[String] = "abc".split(" ", 2) + val x6: String = "abc".toLowerCase() + val x7: String = "abc".toUpperCase() + val x8: String = "abc ".trim() + val x9: Array[Char] = "abc".toCharArray() + val x10: String = "abc".substring(1) +} From d0ca9055ad69e6a1e6b913bcc4092b157f6d2cee Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 16:03:05 -0400 Subject: [PATCH 035/127] Fix tests/pos/t4579.scala --- tests/pos/t4579.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/t4579.scala b/tests/pos/t4579.scala index 500ffae40200..2ab202436b98 100644 --- a/tests/pos/t4579.scala +++ b/tests/pos/t4579.scala @@ -97,7 +97,7 @@ object LispCaseClasses extends Lisp { x6: Data, x7: Data, x8: Data, x9: Data): Data = CONS(x0, list(x1, x2, x3, x4, x5, x6, x7, x8, x9)); - var curexp: Data = null + var curexp: Data = ??? var trace: Boolean = false var indent: Int = 0 From 406dcb38c5ea478ce1b348568a52873979b61e5b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 16:08:35 -0400 Subject: [PATCH 036/127] Fix tests/pos/extractors.scala --- tests/pos/extractors.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/extractors.scala b/tests/pos/extractors.scala index 8e5b0e9d2b68..495dfdafda71 100644 --- a/tests/pos/extractors.scala +++ b/tests/pos/extractors.scala @@ -14,14 +14,14 @@ object test { } } - trait ApplyDeconstructor extends DeconstructorCommon[Apply] { + trait ApplyDeconstructor extends DeconstructorCommon[Apply|Null] { def _1: Tree def _2: List[Tree] } object Apply extends ApplyDeconstructor { - def _1: Tree = field.fun - def _2: List[Tree] = field.args + def _1: Tree = field.asInstanceOf[Apply].fun + def _2: List[Tree] = field.asInstanceOf[Apply].args } def assocsFromApply(tree: Tree) = { From 266e92f3d41ec592637ab95a7e2f3fe181cbffa0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 17:16:35 -0400 Subject: [PATCH 037/127] Fix tests/pos/Meter.scala and tests/pos/extmethods.scala --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/Types.scala | 33 +++++++++++-------- tests/pos/Meter.scala | 2 +- tests/pos/extmethods.scala | 2 +- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 8c9d25c931b3..047687a982d3 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -382,6 +382,7 @@ object StdNames { val asClass: N = "asClass" val asInstanceOf_ : N = "asInstanceOf" val assert_ : N = "assert" + val asSubclass: N = "asSubclass" val assume_ : N = "assume" val box: N = "box" val build : N = "build" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index aa030e6f54f6..0732f3dee60f 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4817,6 +4817,9 @@ object Types { } } + /** A policy that nullifies only method parameters (but not result types). */ + def paramsOnlyP(trigger: Symbol => Boolean): MethodP = MethodP(trigger, nlfyParams = true, nlfyRes = false) + /** A wrapper policy that works as `inner` but additionally verifies that the symbol is contained in `owner`. */ case class WithinSym(inner: NullifyPolicy, owner: Symbol) extends NullifyPolicy { override def isApplicable(sym: Symbol): Boolean = sym.owner == owner && inner.isApplicable(sym) @@ -4830,23 +4833,27 @@ object Types { FieldP(_.name == nme.TYPE_), // The `toString` method. MethodP(_.name == nme.toString_), - // The `newInstance` method in `Class`. - WithinSym(MethodP(_.name == nme.newInstance), defn.ClassClass), // Constructors: params are nullified, but the result type isn't. MethodP(_.isConstructor, nlfyParams = true, nlfyRes = false) ) ++ Seq( // Methods in `java.lang.String`. - MethodP(_.name == nme.concat, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.replace, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.replaceFirst, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.replaceAll, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.split, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.toLowerCase, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.toUpperCase, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.trim, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.toCharArray, nlfyParams = true, nlfyRes = false), - MethodP(_.name == nme.substring, nlfyParams = true, nlfyRes = false) - ).map(WithinSym(_, defn.StringClass)) + paramsOnlyP(_.name == nme.concat), + paramsOnlyP(_.name == nme.replace), + paramsOnlyP(_.name == nme.replaceFirst), + paramsOnlyP(_.name == nme.replaceAll), + paramsOnlyP(_.name == nme.split), + paramsOnlyP(_.name == nme.toLowerCase), + paramsOnlyP(_.name == nme.toUpperCase), + paramsOnlyP(_.name == nme.trim), + paramsOnlyP(_.name == nme.toCharArray), + paramsOnlyP(_.name == nme.substring) + ).map(WithinSym(_, defn.StringClass)) ++ Seq( + // Methods in `java.lang.Class` + paramsOnlyP(_.name == nme.newInstance), + paramsOnlyP(_.name == nme.asSubclass), + paramsOnlyP(_.name == jnme.ForName) + ).map(WithinSym(_, defn.ClassClass)) + val (fromWhitelistTp, handled) = whitelist.foldLeft((tp, false)) { case (res@(_, true), _) => res diff --git a/tests/pos/Meter.scala b/tests/pos/Meter.scala index c32d6a4142f1..272062760065 100644 --- a/tests/pos/Meter.scala +++ b/tests/pos/Meter.scala @@ -7,7 +7,7 @@ package a { class Meter(val underlying: Double) extends AnyVal with _root_.b.Printable { def + (other: Meter): Meter = new Meter(this.underlying + other.underlying) - def / (other: Meter)(implicit dummy: Meter.MeterArg = null): Double = this.underlying / other.underlying + def / (other: Meter)(implicit dummy: Meter.MeterArg = ???): Double = this.underlying / other.underlying def / (factor: Double): Meter = new Meter(this.underlying / factor) def < (other: Meter): Boolean = this.underlying < other.underlying def toFoot: Foot = new Foot(this.underlying * 0.3048) diff --git a/tests/pos/extmethods.scala b/tests/pos/extmethods.scala index fe95a1c79b04..c0f8664247d3 100644 --- a/tests/pos/extmethods.scala +++ b/tests/pos/extmethods.scala @@ -15,7 +15,7 @@ object CollectionStrawMan { implicit class ArrayOps[A](val xs: Array[A]) extends AnyVal { - def elemTag: ClassTag[A] = ClassTag(xs.getClass.getComponentType) + def elemTag: ClassTag[A] = ClassTag(xs.getClass.getComponentType.asInstanceOf[Class[A]]) protected[this] def newBuilder = new ArrayBuffer[A].mapResult(_.toArray(elemTag)) } From 786038daf632794210a872d421dd6abcd56516e4 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 17:37:53 -0400 Subject: [PATCH 038/127] Factor out java nullability transform into its own file --- .../src/dotty/tools/dotc/core/JavaNull.scala | 156 ++++++++++++++++++ .../src/dotty/tools/dotc/core/Types.scala | 146 +--------------- .../dotc/core/classfile/ClassfileParser.scala | 2 +- .../src/dotty/tools/dotc/typer/Namer.scala | 2 +- 4 files changed, 159 insertions(+), 147 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/core/JavaNull.scala diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala new file mode 100644 index 000000000000..3aed99fb4137 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -0,0 +1,156 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Flags.JavaDefined +import dotty.tools.dotc.core.StdNames.{jnme, nme} +import dotty.tools.dotc.core.Symbols.{Symbol, defn, _} +import dotty.tools.dotc.core.Types.{AppliedType, LambdaType, MethodType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef} + +/** Transformation from Java (nullable) to Scala (non-nullable) types */ +object JavaNull { + + /** Adds nullability annotations to a Java-defined member. + * `tp` is the member type. The type inside `sym` shouldn't be used (might not be even set). + */ + def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { + assert(sym.is(JavaDefined), s"can only nullify java-defined members") + + // A list of members that are special-cased. + val whitelist: Seq[NullifyPolicy] = Seq( + // The `TYPE` field in every class. + FieldP(_.name == nme.TYPE_), + // The `toString` method. + MethodP(_.name == nme.toString_), + // Constructors: params are nullified, but the result type isn't. + paramsOnlyP(_.isConstructor) + ) ++ Seq( + // Methods in `java.lang.String`. + paramsOnlyP(_.name == nme.concat), + paramsOnlyP(_.name == nme.replace), + paramsOnlyP(_.name == nme.replaceFirst), + paramsOnlyP(_.name == nme.replaceAll), + paramsOnlyP(_.name == nme.split), + paramsOnlyP(_.name == nme.toLowerCase), + paramsOnlyP(_.name == nme.toUpperCase), + paramsOnlyP(_.name == nme.trim), + paramsOnlyP(_.name == nme.toCharArray), + paramsOnlyP(_.name == nme.substring) + ).map(WithinSym(_, defn.StringClass)) ++ Seq( + // Methods in `java.lang.Class` + paramsOnlyP(_.name == nme.newInstance), + paramsOnlyP(_.name == nme.asSubclass), + paramsOnlyP(_.name == jnme.ForName) + ).map(WithinSym(_, defn.ClassClass)) + + + val (fromWhitelistTp, handled) = whitelist.foldLeft((tp, false)) { + case (res@(_, true), _) => res + case ((_, false), pol) => + if (pol.isApplicable(sym)) (pol(tp), true) + else (tp, false) + } + + if (handled) { + fromWhitelistTp + } else { + // Default case: nullify everything. + val nullMap = new JavaNullMap() + nullMap(tp) + } + } + + /** A policy that special cases the handling of some symbol or class of symbols. */ + sealed trait NullifyPolicy { + /** Whether the policy applies to `sym`. */ + def isApplicable(sym: Symbol): Boolean + /** Nullifies `tp` according to the policy. Should call `isApplicable` first. */ + def apply(tp: Type): Type + } + + /** A policy that avoids modifying a field. */ + case class FieldP(trigger: Symbol => Boolean)(implicit ctx: Context) extends NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = trigger(sym) + override def apply(tp: Type): Type = { + assert(!tp.isJavaMethod, s"FieldPolicy applies to method (non-field) type ${tp.show}") + tp + } + } + + /** A policy for handling a method or poly. Can indicate whether the argument or return types should be nullified. */ + case class MethodP(trigger: Symbol => Boolean, + nlfyParams: Boolean = false, + nlfyRes: Boolean = false) + (implicit ctx: Context) extends TypeMap with NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = trigger(sym) + + override def apply(tp: Type): Type = { + tp match { + case ptp: PolyType => + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) + case mtp: MethodType => + val nullMap = new JavaNullMap() + val paramTpes = if (nlfyParams) mtp.paramInfos.mapConserve(nullMap) else mtp.paramInfos + val resTpe = if (nlfyRes) nullMap(mtp.resType) else mtp.resType + derivedLambdaType(mtp)(paramTpes, resTpe) + } + } + } + + /** A policy that nullifies only method parameters (but not result types). */ + def paramsOnlyP(trigger: Symbol => Boolean)(implicit ctx: Context): MethodP = { + MethodP(trigger, nlfyParams = true, nlfyRes = false) + } + + /** A wrapper policy that works as `inner` but additionally verifies that the symbol is contained in `owner`. */ + case class WithinSym(inner: NullifyPolicy, owner: Symbol)(implicit ctx: Context) extends NullifyPolicy { + override def isApplicable(sym: Symbol): Boolean = sym.owner == owner && inner.isApplicable(sym) + + override def apply(tp: Type): Type = inner(tp) + } + + /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact + * that Java types remain nullable by default. + * + * nullify(T) = T | JavaNull if T is a type parameter or class or interface + * nullify(C[S]) = C[nullify(S)] | JavaNull if C is a generic class + */ + class JavaNullMap(implicit ctx: Context) extends TypeMap { + def shouldNullify(tp: Type): Boolean = { + tp match { + case tp: TypeRef => + !tp.symbol.isValueClass && + !tp.symbol.derivesFrom(defn.AnnotationClass) && + !tp.isRef(defn.ObjectClass) && + !tp.isRef(defn.AnyClass) + case _ => + true + } + } + + def shouldDescend(tp: AppliedType): Boolean = { + val AppliedType(tycons, _) = tp + // Since `Class` objects are runtime representations of _classes_, it doesn't make + // sense to talk about e.g. Class[String | JavaNull], so we don't recursive inside `Class` + // while nullifying things. + tycons != defn.ClassClass.typeRef + } + + override def apply(tp: Type): Type = { + tp match { + case tp: LambdaType => + mapOver(tp) + case tp: TypeAlias => + mapOver(tp) + case tp: TypeRef if shouldNullify(tp) => + defn.javaNullable(tp) + case tp: TypeParamRef => + defn.javaNullable(tp) + case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => + val targs2 = if (shouldDescend(appTp)) targs map this else targs + defn.javaNullable(AppliedType(tycons, targs2)) + case _ => + tp + } + } + } +} diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 0732f3dee60f..f5e98b528b79 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4774,151 +4774,7 @@ object Types { protected def reapply(tp: Type): Type = apply(tp) } - - /** Adds nullability annotations to a Java-defined member. - * `tp` is the member type. The type inside `sym` shouldn't be used (might not be even set). - */ - def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { - assert(sym.is(JavaDefined), s"can only nullify java-defined members") - - /** A policy that special cases the handling of some symbol or class of symbols. */ - sealed trait NullifyPolicy { - /** Whether the policy applies to `sym`. */ - def isApplicable(sym: Symbol): Boolean - /** Nullifies `tp` according to the policy. Should call `isApplicable` first. */ - def apply(tp: Type): Type - } - - /** A policy that avoids modifying a field. */ - case class FieldP(trigger: Symbol => Boolean) extends NullifyPolicy { - override def isApplicable(sym: Symbol): Boolean = trigger(sym) - override def apply(tp: Type): Type = { - assert(!tp.isJavaMethod, s"FieldPolicy applies to method (non-field) type ${tp.show}") - tp - } - } - - /** A policy for handling a method or poly. Can indicate whether the argument or return types should be nullified. */ - case class MethodP(trigger: Symbol => Boolean, - nlfyParams: Boolean = false, - nlfyRes: Boolean = false) extends TypeMap with NullifyPolicy { - override def isApplicable(sym: Symbol): Boolean = trigger(sym) - - override def apply(tp: Type): Type = { - tp match { - case ptp: PolyType => - derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) - case mtp: MethodType => - val nullMap = new JavaNullMap() - val paramTpes = if (nlfyParams) mtp.paramInfos.mapConserve(nullMap) else mtp.paramInfos - val resTpe = if (nlfyRes) nullMap(mtp.resType) else mtp.resType - derivedLambdaType(mtp)(paramTpes, resTpe) - } - } - } - - /** A policy that nullifies only method parameters (but not result types). */ - def paramsOnlyP(trigger: Symbol => Boolean): MethodP = MethodP(trigger, nlfyParams = true, nlfyRes = false) - - /** A wrapper policy that works as `inner` but additionally verifies that the symbol is contained in `owner`. */ - case class WithinSym(inner: NullifyPolicy, owner: Symbol) extends NullifyPolicy { - override def isApplicable(sym: Symbol): Boolean = sym.owner == owner && inner.isApplicable(sym) - - override def apply(tp: Type): Type = inner(tp) - } - - // A list of members that are special-cased. - val whitelist: Seq[NullifyPolicy] = Seq( - // The `TYPE` field in every class. - FieldP(_.name == nme.TYPE_), - // The `toString` method. - MethodP(_.name == nme.toString_), - // Constructors: params are nullified, but the result type isn't. - MethodP(_.isConstructor, nlfyParams = true, nlfyRes = false) - ) ++ Seq( - // Methods in `java.lang.String`. - paramsOnlyP(_.name == nme.concat), - paramsOnlyP(_.name == nme.replace), - paramsOnlyP(_.name == nme.replaceFirst), - paramsOnlyP(_.name == nme.replaceAll), - paramsOnlyP(_.name == nme.split), - paramsOnlyP(_.name == nme.toLowerCase), - paramsOnlyP(_.name == nme.toUpperCase), - paramsOnlyP(_.name == nme.trim), - paramsOnlyP(_.name == nme.toCharArray), - paramsOnlyP(_.name == nme.substring) - ).map(WithinSym(_, defn.StringClass)) ++ Seq( - // Methods in `java.lang.Class` - paramsOnlyP(_.name == nme.newInstance), - paramsOnlyP(_.name == nme.asSubclass), - paramsOnlyP(_.name == jnme.ForName) - ).map(WithinSym(_, defn.ClassClass)) - - - val (fromWhitelistTp, handled) = whitelist.foldLeft((tp, false)) { - case (res@(_, true), _) => res - case ((_, false), pol) => - if (pol.isApplicable(sym)) (pol(tp), true) - else (tp, false) - } - - if (handled) { - fromWhitelistTp - } else { - // Default case: nullify everything. - val nullMap = new JavaNullMap() - nullMap(tp) - } - } - - /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact - * that Java types remain nullable by default. - * - * nullify(T) = T | JavaNull if T is a type parameter or class or interface - * nullify(C[S]) = C[nullify(S)] | JavaNull if C is a generic class - */ - class JavaNullMap(implicit ctx: Context) extends TypeMap { - def shouldNullify(tp: Type): Boolean = { - tp match { - case tp: TypeRef => - !tp.symbol.isValueClass && - !tp.symbol.derivesFrom(defn.AnnotationClass) && - !tp.isRef(defn.ObjectClass) && - !tp.isRef(defn.AnyClass) - case _ => - true - } - } - - def shouldDescend(tp: AppliedType): Boolean = { - val AppliedType(tycons, _) = tp - // Since `Class` objects are runtime representations of _classes_, it doesn't make - // sense to talk about e.g. Class[String | JavaNull], so we don't recursive inside `Class` - // while nullifying things. - tycons != defn.ClassClass.typeRef - } - - override def apply(tp: Type): Type = { - tp match { - case tp: MethodType => - mapOver(tp) - case tp: TypeAlias => - mapOver(tp) - case tp: TypeRef if shouldNullify(tp) => - defn.javaNullable(tp) - case tp@RefinedType(parent, name, info) => // TODO(abeln): does this ever happen for Java-sourced types? - defn.javaNullable(derivedRefinedType(tp, parent, this(info))) - case tp: TypeParamRef => - defn.javaNullable(tp) - case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => - val targs2 = if (shouldDescend(appTp)) targs map this else targs - defn.javaNullable(AppliedType(tycons, targs2)) - case _ => - tp - } - } - } - + /** A range of possible types between lower bound `lo` and upper bound `hi`. * Only used internally in `ApproximatingTypeMap`. */ diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 90c2c4bdfb4f..fa49a2825fa9 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -266,7 +266,7 @@ class ClassfileParser( def nullifyTpe(): Unit = { val old = denot.info - denot.info = nullifyMember(denot.symbol, denot.info) + denot.info = JavaNull.nullifyMember(denot.symbol, denot.info) Printers.nullability.println(s"nullified member type from classfile for ${denot.symbol.name.show} from ${old.show} into ${denot.info.show}") } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 804ca2cbd678..36b6fa339eb5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1179,7 +1179,7 @@ class Namer { typer: Typer => } val memTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) if (mdef.mods.is(JavaDefined)) { - val nullifiedTpe = nullifyMember(sym, memTpe) + val nullifiedTpe = JavaNull.nullifyMember(sym, memTpe) Printers.nullability.println(s"nullified member type from source for ${sym.name.show} from ${memTpe.show} into ${nullifiedTpe.show}") nullifiedTpe } else { From 528dce0ea1cd289a993c27024406b5b444cfe188 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 18:19:15 -0400 Subject: [PATCH 039/127] Fixed tests/pos/explicitOuter.scala --- tests/pos/explicitOuter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/explicitOuter.scala b/tests/pos/explicitOuter.scala index 44b441956420..2ca17fb7c545 100644 --- a/tests/pos/explicitOuter.scala +++ b/tests/pos/explicitOuter.scala @@ -51,7 +51,7 @@ class Outer(elem: Int, val next: Outer) { object Test extends App { - val o = new Outer(1, new Outer(2, null)) + val o = new Outer(1, new Outer(2, ???)) val ic = new o.InnerClass(1) println(ic.bar) println(ic.foo) From 10e1454f75b17cc4101d92e0564dc878d935eb5d Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 22 Oct 2018 18:24:15 -0400 Subject: [PATCH 040/127] Fix tests/pos/i2732.scala --- tests/pos/i2732.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/i2732.scala b/tests/pos/i2732.scala index a37634485409..ba7d131c6b5b 100644 --- a/tests/pos/i2732.scala +++ b/tests/pos/i2732.scala @@ -1,5 +1,5 @@ object Test { val f: java.util.function.Function[_ >: String, _ <: Int] = str => 1 - val i: Int = f("") + val i: Int|Null = f("") } From 31e9bc3204862e179039446a0a073a744b33ccd9 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 25 Oct 2018 12:19:45 -0400 Subject: [PATCH 041/127] Revert changes to equality Allow comparisons of the form `x == null`, even when the type of `x` is non-nullable (e.g. String). This is so that if a null value managed to sneak in, we can root it out with equality comparisons (our type system is unsound in some corner cases). --- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 7 +++++-- tests/neg/explicit-null-default.scala | 6 +----- tests/pos/explicit-null-eq.scala | 9 +++++++++ tests/pos/i1793.scala | 5 +++-- 4 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 tests/pos/explicit-null-eq.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index a1763bd8f859..25f4a396f2fd 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -851,8 +851,11 @@ trait Implicits { self: Typer => if (ltp.isRef(defn.NullClass)) rtp else if (rtp.isRef(defn.NullClass)) ltp else NoType - - (other ne NoType) && other.classSymbol.isNullableClass + // Even though classes deriving from `AnyRef` are non-nullable, we still + // allow testing them for equality against null. + // This is because nulls can still sneak in without detection by the type + // system (e.g. via Java), so checking for null is a useful escape hatch. + (other ne NoType) && !other.derivesFrom(defn.AnyValClass) } // Map all non-opaque abstract types to their upper bound. diff --git a/tests/neg/explicit-null-default.scala b/tests/neg/explicit-null-default.scala index fddc9dd98af4..fe115861e926 100644 --- a/tests/neg/explicit-null-default.scala +++ b/tests/neg/explicit-null-default.scala @@ -6,12 +6,8 @@ class Foo { val y = foo(null) // error: String argument is non-nullable - val z = foo("hello") - - if (z == null) { // error: z is non-nullable - } + val z: String = foo("hello") class Bar - val b: Bar = null // error: user-created classes are also non-nullable } diff --git a/tests/pos/explicit-null-eq.scala b/tests/pos/explicit-null-eq.scala new file mode 100644 index 000000000000..883e557bb2bd --- /dev/null +++ b/tests/pos/explicit-null-eq.scala @@ -0,0 +1,9 @@ +// Test that non-nullable types can be still be compared +// for equality against null. +class Foo { + val x: String = "hello" + if (x != null) { // allowed as escape hatch + } + if (x == null) { + } +} diff --git a/tests/pos/i1793.scala b/tests/pos/i1793.scala index fed8a6165699..7c7eac1bfa33 100644 --- a/tests/pos/i1793.scala +++ b/tests/pos/i1793.scala @@ -1,7 +1,8 @@ object Test { import scala.ref.WeakReference def unapply[T <: AnyRef](wr: WeakReference[T]): Option[T] = { - val x = wr.underlying.get - if (x != null) Some(x) else None + val x: T|Null = wr.underlying.get + val x2 = x.asInstanceOf[T] + if (x2 != null) Some(x2) else None } } From c8a95af7a8a32ef1cd7b6e1543c53236befb1895 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 25 Oct 2018 12:34:51 -0400 Subject: [PATCH 042/127] Fix tests/pos/i1754.scala --- tests/pos/i1754.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/i1754.scala b/tests/pos/i1754.scala index bb0da32671d3..37b27e4e96be 100644 --- a/tests/pos/i1754.scala +++ b/tests/pos/i1754.scala @@ -16,9 +16,9 @@ object Test { * @param m The ConcurrentMap to be converted. * @return A Scala mutable ConcurrentMap view of the argument. */ - implicit def mapAsScalaConcurrentMap[A, B](m: juc.ConcurrentMap[A, B]): concurrent.Map[A, B] = m match { + implicit def mapAsScalaConcurrentMap[A, B](m: juc.ConcurrentMap[A, B]|Null): concurrent.Map[A, B]|Null = m match { case null => null case cmw: ConcurrentMapWrapper[_, _] => cmw.underlying - case _ => new JConcurrentMapWrapper(m) + case cm: juc.ConcurrentMap[A, B] => new JConcurrentMapWrapper(cm) } } From 66b780190cfa99e4e61bcf8e0ada36e589f08e45 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 25 Oct 2018 12:43:03 -0400 Subject: [PATCH 043/127] Fix tests/pos/virtpatmat_gadt_array.scala --- tests/pos/explicit-null-array.scala | 5 +++++ tests/pos/virtpatmat_gadt_array.scala | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/pos/explicit-null-array.scala diff --git a/tests/pos/explicit-null-array.scala b/tests/pos/explicit-null-array.scala new file mode 100644 index 000000000000..f3146c8e8e2b --- /dev/null +++ b/tests/pos/explicit-null-array.scala @@ -0,0 +1,5 @@ +// Test that array contents are non-nullable. +class Foo { + val x: Array[String] = Array("hello") + val s: String = x(0) +} diff --git a/tests/pos/virtpatmat_gadt_array.scala b/tests/pos/virtpatmat_gadt_array.scala index 02dbad68d9d0..dcd3f02bb8fe 100644 --- a/tests/pos/virtpatmat_gadt_array.scala +++ b/tests/pos/virtpatmat_gadt_array.scala @@ -1,6 +1,6 @@ import scala.collection.mutable._ object Test { - def genericArrayOps[T](xs: Array[T]): ArrayOps[T] = xs match { + def genericArrayOps[T](xs: Array[T]|Null): ArrayOps[T]|Null = xs match { case x: Array[AnyRef] => refArrayOps[AnyRef](x).asInstanceOf[ArrayOps[T]] case null => null } From 04764fc3a62d7aa7d39ae475f7f070cb9c7904e5 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 25 Oct 2018 13:55:32 -0400 Subject: [PATCH 044/127] Fix tests/pos/t1001.scala --- compiler/src/dotty/tools/dotc/core/JavaNull.scala | 2 +- tests/pos/t1001.scala | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index 3aed99fb4137..65b28ce15703 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -39,7 +39,7 @@ object JavaNull { // Methods in `java.lang.Class` paramsOnlyP(_.name == nme.newInstance), paramsOnlyP(_.name == nme.asSubclass), - paramsOnlyP(_.name == jnme.ForName) + paramsOnlyP(_.name == jnme.ForName), ).map(WithinSym(_, defn.ClassClass)) diff --git a/tests/pos/t1001.scala b/tests/pos/t1001.scala index 7a06bfa0e22d..74a86b7bd685 100644 --- a/tests/pos/t1001.scala +++ b/tests/pos/t1001.scala @@ -1,9 +1,11 @@ // was t1001.scala class Foo; +import java.lang.reflect.Constructor + object Overload{ - val foo = classOf[Foo].getConstructors()(0) - foo.getDeclaringClass + val foo: Constructor[_]|Null = classOf[Foo].getConstructors()(0) + foo.asInstanceOf[Constructor[_]].getDeclaringClass } // was t1001.scala From ef6cf46c7af818172c8699bc32d45385cc2a8a90 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 25 Oct 2018 15:40:11 -0400 Subject: [PATCH 045/127] Fix some more tests --- tests/pos/t6942/t6942.scala | 2 +- tests/pos/ticket2201.scala | 2 +- tests/pos/virtpatmat_exist3.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/t6942/t6942.scala b/tests/pos/t6942/t6942.scala index 8d729122fe04..bb5d083c53bc 100644 --- a/tests/pos/t6942/t6942.scala +++ b/tests/pos/t6942/t6942.scala @@ -2,7 +2,7 @@ // its budget should suffice for these simple matches (they do have a large search space) class Test { import foo.Bar // a large enum - def exhaustUnreachabilitysStack_ENUM_STYLE = (???: Bar) match { + def exhaustUnreachabilitysStack_ENUM_STYLE = (??? : Bar) match { case Bar.BULGARIA => case _ => } diff --git a/tests/pos/ticket2201.scala b/tests/pos/ticket2201.scala index 96b7b4a3d493..f0b48f17b2d2 100644 --- a/tests/pos/ticket2201.scala +++ b/tests/pos/ticket2201.scala @@ -2,7 +2,7 @@ class Test object Test { implicit def view(x : Test): Int = 0 } object Call { - def call(implicit view : Test => Int) = view(null) + def call(implicit view : Test => Int) = view(???) call call } diff --git a/tests/pos/virtpatmat_exist3.scala b/tests/pos/virtpatmat_exist3.scala index 6a6d428b1a09..d96d512f04d1 100644 --- a/tests/pos/virtpatmat_exist3.scala +++ b/tests/pos/virtpatmat_exist3.scala @@ -1,5 +1,5 @@ class ReferenceQueue[T] { - def wrapper(jref: ReferenceQueue[_]): ReferenceQueue[T] = + def wrapper(jref: ReferenceQueue[_]|Null): ReferenceQueue[T]|Null = jref match { case null => null } From 53bdbc46117b5258d812b6d358482a630fa9dab6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 30 Oct 2018 15:35:15 -0400 Subject: [PATCH 046/127] Fix two more tests --- tests/pos/tcpoly_seq_typealias.scala | 2 +- tests/pos/tuplePatDef.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/tcpoly_seq_typealias.scala b/tests/pos/tcpoly_seq_typealias.scala index 25ec38065668..9d79825f4c1a 100644 --- a/tests/pos/tcpoly_seq_typealias.scala +++ b/tests/pos/tcpoly_seq_typealias.scala @@ -69,7 +69,7 @@ trait HOSeq { start = last } else { val last1 = last - last = new HOSeq.this.:: (x, null) // hack: ::'s tail will actually be last + last = new HOSeq.this.:: (x, ???) // hack: ::'s tail will actually be last //last1.tl = last } } diff --git a/tests/pos/tuplePatDef.scala b/tests/pos/tuplePatDef.scala index 22f8f8e7d6ed..d38f3120169f 100644 --- a/tests/pos/tuplePatDef.scala +++ b/tests/pos/tuplePatDef.scala @@ -1,4 +1,4 @@ object Test { - val (x,y): (String, Int) = null + val (x,y): (String, Int) = ??? } From 2c4e9913d3c84a6807d00edb8ec71e37dfe38ac9 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 30 Oct 2018 18:23:39 -0400 Subject: [PATCH 047/127] Fix tests/pos/i1044.scala --- tests/pos/i1044.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/pos/i1044.scala b/tests/pos/i1044.scala index a984dbd67449..8f8619c23f4d 100644 --- a/tests/pos/i1044.scala +++ b/tests/pos/i1044.scala @@ -1,3 +1,7 @@ object Test { - val x = ???.getClass.getMethods.head.getParameterTypes.mkString(",") + implicit class NN[T](x: T|Null) { + def nn: T = x.asInstanceOf[T] + } + + val x = ???.getClass.getMethods.nn.head.nn.getParameterTypes.nn.mkString(",") } From 06caedd397e6b88205ba0e5e7a25884de500583d Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 6 Nov 2018 12:26:18 -0500 Subject: [PATCH 048/127] Allow prototypes of the form T|Null to be used for type inference --- .../dotty/tools/dotc/core/Definitions.scala | 2 ++ .../src/dotty/tools/dotc/core/Types.scala | 23 +++++++++++++------ .../dotty/tools/dotc/typer/ProtoTypes.scala | 6 ++++- tests/pos/explicit-null-or-prototype.scala | 8 +++++++ 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 tests/pos/explicit-null-or-prototype.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 0ad17c370635..08b89a5f64c7 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -354,6 +354,8 @@ class Definitions { lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol + def nullable(tp: Type) = OrType(tp, NullType) + lazy val Predef_ConformsR: TypeRef = ScalaPredefModule.requiredClass("<:<").typeRef def Predef_Conforms(implicit ctx: Context): Symbol = Predef_ConformsR.symbol lazy val Predef_conformsR: TermRef = ScalaPredefModule.requiredMethodRef(nme.conforms_) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f5e98b528b79..3f522937c6bf 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -282,12 +282,21 @@ object Types { case _ => false } - /** Is this type `JavaNullType` */ + /** Is this type `JavaNullType`? */ def isJavaNull(implicit ctx: Context): Boolean = this == defn.JavaNullType /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ - def isJavaNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeJavaNull match { - case OrType(_, right) if right.isJavaNull => true + def isJavaNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + case OrType(_, right) => right.isJavaNull + case _ => false + } + + /** Is this type `NullType`? */ + def isNull(implicit ctx: Context): Boolean = this.isRef(defn.NullClass) + + /** Is this (after widening and dealiasing) a type of the form `T | Null`? */ + def isNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + case OrType(_, right) => right.isNull case _ => false } @@ -990,15 +999,15 @@ object Types { } /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ - def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeJavaNull match { + def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { case OrType(left, right) if right.isJavaNull => left.stripJavaNull case _ => this } - /** Converts types of the form `JavaNull | T` to `T | JavaNull`. Does not do any widening or dealiasing. */ - def normalizeJavaNull(implicit ctx: Context): Type = { + /** Converts types of the form `Null | T` to `T | Null`. Does not do any widening or dealiasing. */ + def normalizeNull(implicit ctx: Context): Type = { this match { - case OrType(left, right) if left.isJavaNull => defn.javaNullable(right) + case OrType(left, right) if left.isNull => OrType(right, left) case _ => this } } diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 9c3af1eef743..869dda47d2db 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -42,7 +42,11 @@ object ProtoTypes { } private def disregardProto(pt: Type)(implicit ctx: Context): Boolean = pt.dealias match { - case _: OrType => true + case orTpe: OrType => + // If either side of the union is the `Null` type, it should be safe to not disregard the prototype. + // This is because checking for compatibility against e.g. `OrType(T1, Null)` will not generate incompatible + // constraints (because `Null` is atomic). + !orTpe.isNullable case pt => pt.isRef(defn.UnitClass) } diff --git a/tests/pos/explicit-null-or-prototype.scala b/tests/pos/explicit-null-or-prototype.scala new file mode 100644 index 000000000000..60ca9a766f87 --- /dev/null +++ b/tests/pos/explicit-null-or-prototype.scala @@ -0,0 +1,8 @@ + +class Foo { + // Test that prototypes of the form "SomeType | Null" are used in type inference. + // Otherwise, the application of `Array` below would be typed as `Array.apply[String](x)`, + // which doesn't typecheck. + def foo(x: Array[String|Null]|Null) = 0 + foo(Array("hello")) +} From b62eef11a4e234c73253e33e408bdfb645fd765f Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 6 Nov 2018 15:18:16 -0500 Subject: [PATCH 049/127] Improve naming of nullability convenience functions --- .../dotty/tools/dotc/core/Definitions.scala | 2 - .../src/dotty/tools/dotc/core/JavaNull.scala | 6 +- .../tools/dotc/core/TypeApplications.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 72 ++++++++++--------- .../core/unpickleScala2/Scala2Unpickler.scala | 2 +- .../tools/dotc/transform/FirstTransform.scala | 2 +- .../dotty/tools/dotc/typer/ProtoTypes.scala | 2 +- 7 files changed, 46 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 08b89a5f64c7..6c34635e6d7e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -349,8 +349,6 @@ class Definitions { lazy val JavaNull = enterAliasType(tpnme.JavaNull, AnnotatedType.make(NullType, List(Annotation(JavaNullAnnot)))) def JavaNullType = JavaNull.typeRef - def javaNullable(tp: Type) = OrType(tp, JavaNullType) - lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index 65b28ce15703..3492ff22c514 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -142,12 +142,12 @@ object JavaNull { case tp: TypeAlias => mapOver(tp) case tp: TypeRef if shouldNullify(tp) => - defn.javaNullable(tp) + tp.toJavaNullable case tp: TypeParamRef => - defn.javaNullable(tp) + tp.toJavaNullable case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => val targs2 = if (shouldDescend(appTp)) targs map this else targs - defn.javaNullable(AppliedType(tycons, targs2)) + AppliedType(tycons, targs2).toJavaNullable case _ => tp } diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 5f861cf85ae7..ff0e4516ce9f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -461,7 +461,7 @@ class TypeApplications(val self: Type) extends AnyVal { // If `isJava` is set, then we want to turn `RepeatedParam[T]` into `Array[_ <: T]`, // since arrays aren't covariant until after erasure. See `tests/pos/i5140`. val trans = self1.translateParameterized(defn.RepeatedParamClass, seqClass, wildcardArg = isJava) - if (isJava) defn.javaNullable(trans) else trans + if (isJava) trans.toJavaNullable else trans } else self diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 3f522937c6bf..0e32a388e728 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -236,12 +236,41 @@ object Types { } } - /** Is this type exactly Null (no vars, aliases, refinements etc allowed)? */ + /** Is this type exactly `Null` (no vars, aliases, refinements etc allowed)? */ def isNullType(implicit ctx: Context): Boolean = this match { case tp: TypeRef => tp.symbol eq defn.NullClass case _ => false } + /** Is this type a reference to `Null`, possibly after aliasing? */ + def isRefToNull(implicit ctx: Context): Boolean = this.isRef(defn.NullClass) + + /** Is this (after widening and dealiasing) a type of the form `T | Null`? */ + def isNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + case OrType(_, right) => right.isRefToNull + case _ => false + } + + /** Is this type guaranteed not to have `null` as a value? */ + final def isNotNull(implicit ctx: Context): Boolean = this match { + case tp: ConstantType => tp.value.value != null + case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass + case tp: TypeBounds => tp.lo.isNotNull + case tp: TypeProxy => tp.underlying.isNotNull + case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull + case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull + case _ => false + } + + /** Is this type exactly `JavaNull` (no vars, aliases, refinements etc allowed)? */ + def isJavaNullType(implicit ctx: Context): Boolean = this == defn.JavaNullType + + /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ + def isJavaNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + case OrType(_, right) => right.isJavaNullType + case _ => false + } + /** Is this type exactly Nothing (no vars, aliases, refinements etc allowed)? */ def isBottomType(implicit ctx: Context): Boolean = this match { case tp: TypeRef => tp.symbol eq defn.NothingClass @@ -271,35 +300,6 @@ object Types { loop(this) } - /** Is this type guaranteed not to have `null` as a value? */ - final def isNotNull(implicit ctx: Context): Boolean = this match { - case tp: ConstantType => tp.value.value != null - case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass - case tp: TypeBounds => tp.lo.isNotNull - case tp: TypeProxy => tp.underlying.isNotNull - case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull - case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull - case _ => false - } - - /** Is this type `JavaNullType`? */ - def isJavaNull(implicit ctx: Context): Boolean = this == defn.JavaNullType - - /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ - def isJavaNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { - case OrType(_, right) => right.isJavaNull - case _ => false - } - - /** Is this type `NullType`? */ - def isNull(implicit ctx: Context): Boolean = this.isRef(defn.NullClass) - - /** Is this (after widening and dealiasing) a type of the form `T | Null`? */ - def isNullable(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { - case OrType(_, right) => right.isNull - case _ => false - } - /** Is this type produced as a repair for an error? */ final def isError(implicit ctx: Context): Boolean = stripTypeVar.isInstanceOf[ErrorType] @@ -611,7 +611,7 @@ object Types { case AndType(l, r) => goAnd(l, r) case tp: OrType => - if (tp.isJavaNullable) { + if (tp.isJavaNullableUnion) { // We need to strip JavaNull from both the type and the prefix so that // `pre <: tp` continues to hold. tp.stripJavaNull.findMember(name, pre.stripJavaNull, required, excluded) @@ -1000,14 +1000,14 @@ object Types { /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { - case OrType(left, right) if right.isJavaNull => left.stripJavaNull + case OrType(left, right) if right.isJavaNullType => left.stripJavaNull case _ => this } /** Converts types of the form `Null | T` to `T | Null`. Does not do any widening or dealiasing. */ def normalizeNull(implicit ctx: Context): Type = { this match { - case OrType(left, right) if left.isNull => OrType(right, left) + case OrType(left, right) if left.isRefToNull => OrType(right, left) case _ => this } } @@ -1488,6 +1488,12 @@ object Types { // ----- misc ----------------------------------------------------------- + /** Create a `JavaNullable` version of this type. */ + def toJavaNullable(implicit ctx: Context): Type = { + if (this.isJavaNullableUnion) this + else OrType(this, defn.JavaNullType) + } + /** Turn type into a function type. * @pre this is a method type without parameter dependencies. * @param dropLast The number of trailing parameters that should be dropped diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index e31288d4e16d..edce1ed858de 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -78,7 +78,7 @@ object Scala2Unpickler { elemtp0 } val repParamTp = defn.RepeatedParamType.appliedTo(elemtp) - val lastParamTp = if (lastArg.isJavaNullable) defn.javaNullable(repParamTp) else repParamTp + val lastParamTp = if (lastArg.isJavaNullableUnion) repParamTp.toJavaNullable else repParamTp tp.derivedLambdaType( tp.paramNames, tp.paramInfos.init :+ lastParamTp, diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index cc0fb3b27b76..f4c067d82665 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -50,7 +50,7 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = { tree match { case Select(qual, name) if !name.is(OuterSelectName) && tree.symbol.exists => - val qualTpe = if (qual.tpe.isJavaNullable) qual.tpe.stripJavaNull else qual.tpe + val qualTpe = if (qual.tpe.isJavaNullableUnion) qual.tpe.stripJavaNull else qual.tpe assert( qualTpe.derivesFrom(tree.symbol.owner) || tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass), diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 869dda47d2db..7c5bcda9ce94 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -46,7 +46,7 @@ object ProtoTypes { // If either side of the union is the `Null` type, it should be safe to not disregard the prototype. // This is because checking for compatibility against e.g. `OrType(T1, Null)` will not generate incompatible // constraints (because `Null` is atomic). - !orTpe.isNullable + !orTpe.isNullableUnion case pt => pt.isRef(defn.UnitClass) } From 24a06b98e0b776c3ba19278c330e61c2a615dbd6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 12 Nov 2018 10:32:05 -0500 Subject: [PATCH 050/127] Fix positive tests --- tests/pos/attributes.scala | 4 ++-- tests/pos/protected-static/ScalaClass.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/attributes.scala b/tests/pos/attributes.scala index 60e00bff7d7c..4f9b19f96956 100644 --- a/tests/pos/attributes.scala +++ b/tests/pos/attributes.scala @@ -56,13 +56,13 @@ object O6 { object myAttrs { class a1 extends scala.annotation.Annotation; class a2(x: Int) extends scala.annotation.Annotation; - class a3(x: a1) extends scala.annotation.Annotation; + class a3(x: a1|Null) extends scala.annotation.Annotation; } class a4(ns: Array[Int]) extends scala.annotation.Annotation; object O7 { class a1 extends scala.annotation.Annotation; class a2(x: Int) extends scala.annotation.Annotation; - class a3(x: a1) extends scala.annotation.Annotation; + class a3(x: a1|Null) extends scala.annotation.Annotation; final val x = new a1; @a1 class C1; diff --git a/tests/pos/protected-static/ScalaClass.scala b/tests/pos/protected-static/ScalaClass.scala index 11108b890d42..165e18ada68b 100644 --- a/tests/pos/protected-static/ScalaClass.scala +++ b/tests/pos/protected-static/ScalaClass.scala @@ -2,5 +2,5 @@ import bippy.JavaClass class Implementor extends JavaClass { import JavaClass.Inner - def getInner: Inner = null + def getInner: Inner = ??? } From f4c3695046e9a47c2ae9fdeb21270144c70e613c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 12 Nov 2018 14:51:27 -0500 Subject: [PATCH 051/127] Fix moar tests --- tests/pos/arrays2.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/pos/arrays2.scala b/tests/pos/arrays2.scala index c9e5e0bfc7ad..01fd01a9957d 100644 --- a/tests/pos/arrays2.scala +++ b/tests/pos/arrays2.scala @@ -18,13 +18,13 @@ object arrays4 { // #2461 object arrays3 { import scala.collection.JavaConversions._ - def apply[X](xs : X*) : java.util.List[X] = java.util.Arrays.asList(xs: _*) + def apply[X](xs : X*) : java.util.List[X|Null]|Null = java.util.Arrays.asList(xs: _*) - def apply1[X <: String](xs : X*) : java.util.List[X] = java.util.Arrays.asList(xs: _*) - def apply2[X <: AnyVal](xs : X*) : java.util.List[X] = java.util.Arrays.asList(xs: _*) - def apply3(xs : Int*) : java.util.List[Int] = java.util.Arrays.asList(xs: _*) - def apply4(xs : Unit*) : java.util.List[Unit] = java.util.Arrays.asList(xs: _*) - def apply5(xs : Null*) : java.util.List[Null] = java.util.Arrays.asList(xs: _*) - def apply6(xs : Nothing*) : java.util.List[Nothing] = java.util.Arrays.asList(xs: _*) + def apply1[X <: String](xs : X*) : java.util.List[X|Null]|Null = java.util.Arrays.asList(xs: _*) + def apply2[X <: AnyVal](xs : X*) : java.util.List[X|Null]|Null = java.util.Arrays.asList(xs: _*) + def apply3(xs : Int*) : java.util.List[Int|Null]|Null = java.util.Arrays.asList(xs: _*) + def apply4(xs : Unit*) : java.util.List[Unit|Null]|Null = java.util.Arrays.asList(xs: _*) + def apply5(xs : Null*) : java.util.List[Null]|Null = java.util.Arrays.asList(xs: _*) + def apply6(xs : Nothing*) : java.util.List[Nothing|Null]|Null = java.util.Arrays.asList(xs: _*) } From f1cce5ad125ae282686a230f74ecc8aa40ed272c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 12 Nov 2018 17:08:21 -0500 Subject: [PATCH 052/127] Better prototypes for function literals When typing a function literal, if the prototype is of the form `T|Null`, then use `T` as the prototype (since we know the literal isn't null). --- compiler/src/dotty/tools/dotc/core/Types.scala | 6 ++++++ compiler/src/dotty/tools/dotc/typer/Typer.scala | 17 +++++++++++------ tests/pos/explicit-null-infer-lambda-arg.scala | 11 +++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 tests/pos/explicit-null-infer-lambda-arg.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 0e32a388e728..2b93e8fc4735 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1004,6 +1004,12 @@ object Types { case _ => this } + /** Strips the nullability from a type: `T | Null` goes to `T` */ + def stripNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { + case OrType(left, right) if right.isRefToNull => left.stripNull + case _ => this + } + /** Converts types of the form `Null | T` to `T | Null`. Does not do any widening or dealiasing. */ def normalizeNull(implicit ctx: Context): Type = { this match { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 07a55a9288cf..da7713b8c6fe 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -826,16 +826,21 @@ class Typer extends Namer case _ => false } - pt match { - case pt: TypeVar if untpd.isFunctionWithUnknownParamType(tree) => + // If the prototype is of the form e.g. `Function1[Int, Int]|Null`, then we should be + // able discard the `| Null` from it, since the literal itself isn't nullable. + // This gives us a more precise (useful) prototype. + val pt1 = pt.stripNull + + pt1 match { + case pt1: TypeVar if untpd.isFunctionWithUnknownParamType(tree) => // try to instantiate `pt` if this is possible. If it does not // work the error will be reported later in `inferredParam`, // when we try to infer the parameter type. - isFullyDefined(pt, ForceDegree.noBottom) + isFullyDefined(pt1, ForceDegree.noBottom) case _ => } - val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length) + val (protoFormals, resultTpt) = decomposeProtoFunction(pt1, params.length) def refersTo(arg: untpd.Tree, param: untpd.ValDef): Boolean = arg match { case Ident(name) => name == param.name @@ -916,7 +921,7 @@ class Typer extends Namer } case _ => } - errorType(AnonymousFunctionMissingParamType(param, params, tree, pt), param.pos) + errorType(AnonymousFunctionMissingParamType(param, params, tree, pt1), param.pos) } def protoFormal(i: Int): Type = @@ -947,7 +952,7 @@ class Typer extends Namer inferredParamType(param, protoFormal(i)).underlyingIfRepeated(isJava = false))) desugar.makeClosure(inferredParams, fnBody, resultTpt, isImplicit) } - typed(desugared, pt) + typed(desugared, pt1) } def typedClosure(tree: untpd.Closure, pt: Type)(implicit ctx: Context): Tree = track("typedClosure") { diff --git a/tests/pos/explicit-null-infer-lambda-arg.scala b/tests/pos/explicit-null-infer-lambda-arg.scala new file mode 100644 index 000000000000..8b8ff1f2f40e --- /dev/null +++ b/tests/pos/explicit-null-infer-lambda-arg.scala @@ -0,0 +1,11 @@ +class Foo { + implicit class NN[T](x: T|Null) { + def nn: T = x.asInstanceOf[T] + } + import java.util.ArrayList, java.util.stream.{Stream => JStream} + // Test that we can infer the argument of the lambda in the application of `map`, even + // though the prototype is of the form Function[A,B]|Null + new java.util.ArrayList[String]().stream.map(_.nn.toInt).map(_.nn.toString): JStream[String|Null]|Null + + val x: (Int => Int)|Null = (y) => y +} From 456191aaeadabc9af1c639c57a86825118f5e9a5 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 13 Nov 2018 10:34:41 -0500 Subject: [PATCH 053/127] Don't propagate nullability inside Java generics --- .../src/dotty/tools/dotc/core/JavaNull.scala | 11 ++++++---- tests/pos/explicit-null-generics/J.java | 12 +++++++++++ tests/pos/explicit-null-generics/S.scala | 9 +++++++++ tests/pos/explicit-null-interop-static/J.java | 2 +- tests/pos/i801.scala | 20 ++++++++++++------- 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 tests/pos/explicit-null-generics/J.java create mode 100644 tests/pos/explicit-null-generics/S.scala diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index 3492ff22c514..cbce29137cfb 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -129,10 +129,13 @@ object JavaNull { def shouldDescend(tp: AppliedType): Boolean = { val AppliedType(tycons, _) = tp - // Since `Class` objects are runtime representations of _classes_, it doesn't make - // sense to talk about e.g. Class[String | JavaNull], so we don't recursive inside `Class` - // while nullifying things. - tycons != defn.ClassClass.typeRef + // Only nullify the inside of Scala-defined constructors. + // This is because Java classes are _all_ nullified, so both `java.util.List[String]` and + // `java.util.List[String|Null]` contain nullable elements. + tycons.widenDealias match { + case tp: TypeRef if !tp.symbol.is(JavaDefined) => true + case _ => false + } } override def apply(tp: Type): Type = { diff --git a/tests/pos/explicit-null-generics/J.java b/tests/pos/explicit-null-generics/J.java new file mode 100644 index 000000000000..bd68915a50f8 --- /dev/null +++ b/tests/pos/explicit-null-generics/J.java @@ -0,0 +1,12 @@ + +class I {} + +class J { + I foo(T x) { + return new I(); + } + + ReturnedFromJava foo2(T x) { + return new ReturnedFromJava(); + } +} diff --git a/tests/pos/explicit-null-generics/S.scala b/tests/pos/explicit-null-generics/S.scala new file mode 100644 index 000000000000..53d449e77c77 --- /dev/null +++ b/tests/pos/explicit-null-generics/S.scala @@ -0,0 +1,9 @@ +class ReturnedFromJava[T] {} + +class S { + val j = new J() + // Check that the inside of a Java generic isn't nullified + val i: I[String]|Null = j.foo("hello") + // ... but if the generic is Scala-defined and used from Java, then the |Null _is_ propagated to the inside. + val fromJava: ReturnedFromJava[String|Null]|Null = j.foo2("hello") +} diff --git a/tests/pos/explicit-null-interop-static/J.java b/tests/pos/explicit-null-interop-static/J.java index 94734378b69c..10965aa9ef4c 100644 --- a/tests/pos/explicit-null-interop-static/J.java +++ b/tests/pos/explicit-null-interop-static/J.java @@ -1,4 +1,4 @@ class J { - static int foo(S s) { return 42; } + static int foo(String s) { return 42; } } diff --git a/tests/pos/i801.scala b/tests/pos/i801.scala index 4d8df7f03061..3f6865f9a90e 100644 --- a/tests/pos/i801.scala +++ b/tests/pos/i801.scala @@ -1,9 +1,15 @@ -object T1 { - import java.util.ArrayList, java.util.stream.{Stream => JStream} - new java.util.ArrayList[String]().stream.map(_.toInt).map(_.toString): JStream[String] -} +class Foo { + implicit class NN[T](x: T|Null) { + def nn: T = x.asInstanceOf[T] + } + + object T1 { + import java.util.ArrayList, java.util.stream.{Stream => JStream} + new java.util.ArrayList[String]().stream.map(_.nn.toInt).map(_.nn.toString): JStream[String|Null]|Null + } -object T2 { - import java.util._, java.util.stream.{Stream => JStream} - def f: JStream[String] = new java.util.ArrayList[String](Arrays.asList("1", "2")).stream.map(_.toInt).map(_.toString) + object T2 { + import java.util._, java.util.stream.{Stream => JStream} + def f: JStream[String]|Null = new java.util.ArrayList[String](Arrays.asList[String]("1", "2").nn).stream.map(_.nn.toInt).map(_.nn.toString) + } } From b384d2c6b26710f61d6e73a2db73c6573441ea2a Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 13 Nov 2018 12:23:07 -0500 Subject: [PATCH 054/127] Don't widen union types of the form `T|Null` There are two places where union types are widened: 1) In the r.h.s of member definitions e.g. val x = foo() // foo returns a union 2) When inferring a type parameter foo(if (1 == 2) "hello" else 4) // type parameter inferred to be Any In both of these cases, avoid widening the union if one of its constituent types is `Null`. The main reason for this is that the widening `T|Null` is likely to result in `Any`, which isn't a very useful type. Conversly, it's useful to not lose information about the nullability of a type. --- compiler/src/dotty/tools/dotc/core/Types.scala | 13 ++++++++++--- tests/pos/explicit-null-do-not-widen-1/J.java | 3 +++ tests/pos/explicit-null-do-not-widen-1/S.scala | 7 +++++++ tests/pos/explicit-null-do-not-widen-2.scala | 8 ++++++++ tests/pos/i1045.scala | 4 ++-- 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 tests/pos/explicit-null-do-not-widen-1/J.java create mode 100644 tests/pos/explicit-null-do-not-widen-1/S.scala create mode 100644 tests/pos/explicit-null-do-not-widen-2.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 2b93e8fc4735..2cd11ba5f426 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1087,9 +1087,16 @@ object Types { */ def widenUnion(implicit ctx: Context): Type = this match { case OrType(tp1, tp2) => - ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match { - case union: OrType => union.join - case res => res + if (isNullableUnion) { + normalizeNull match { + case OrType(leftTpe, nullTpe) => OrType(leftTpe.widenUnion, nullTpe) + case tpe => assert(false, s"Expected a union type, but got ${tpe.show}"); tpe + } + } else { + ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match { + case union: OrType => union.join + case res => res + } } case tp @ AndType(tp1, tp2) => tp derived_& (tp1.widenUnion, tp2.widenUnion) diff --git a/tests/pos/explicit-null-do-not-widen-1/J.java b/tests/pos/explicit-null-do-not-widen-1/J.java new file mode 100644 index 000000000000..c957a1f307b6 --- /dev/null +++ b/tests/pos/explicit-null-do-not-widen-1/J.java @@ -0,0 +1,3 @@ +class J { + String foo() { return "hello"; } +} diff --git a/tests/pos/explicit-null-do-not-widen-1/S.scala b/tests/pos/explicit-null-do-not-widen-1/S.scala new file mode 100644 index 000000000000..0fbca30fac0a --- /dev/null +++ b/tests/pos/explicit-null-do-not-widen-1/S.scala @@ -0,0 +1,7 @@ +class S { + val j = new J() + val x = j.foo() + // Check that the type of `x` is inferred to be `String|Null`. + // i.e. the union isn't collapsed. + val y: String|Null = x +} diff --git a/tests/pos/explicit-null-do-not-widen-2.scala b/tests/pos/explicit-null-do-not-widen-2.scala new file mode 100644 index 000000000000..e35615f7079a --- /dev/null +++ b/tests/pos/explicit-null-do-not-widen-2.scala @@ -0,0 +1,8 @@ + +class S { + def foo[T](x: T): T = x + // Check that the type argument to `foo` is inferred to be + // `String|Null`: i.e. it isn't collapsed. + val x = foo(if (1 == 2) "hello" else null) + val y: String|Null = x +} diff --git a/tests/pos/i1045.scala b/tests/pos/i1045.scala index f0cf1df90f8a..57a6a696a126 100644 --- a/tests/pos/i1045.scala +++ b/tests/pos/i1045.scala @@ -16,9 +16,9 @@ object T { class Ident[X >: Null] extends Tree[X] class Apply[X >: Null] extends Tree[X] - val x: Ident[Symbol] | Apply[Symbol] = ??? + val x: Ident[Symbol|Null] | Apply[Symbol|Null] = ??? val y = x.tpe - val z: Symbol = y + val z: Symbol|Null = y } From a6cd806364276615598d7af5ef6e69311b4243e9 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 13 Nov 2018 12:30:05 -0500 Subject: [PATCH 055/127] Fix broken test --- tests/pos/explicit-null-generics/J.java | 5 +---- tests/pos/explicit-null-generics/S.scala | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/pos/explicit-null-generics/J.java b/tests/pos/explicit-null-generics/J.java index bd68915a50f8..b8eab374844b 100644 --- a/tests/pos/explicit-null-generics/J.java +++ b/tests/pos/explicit-null-generics/J.java @@ -5,8 +5,5 @@ class J { I foo(T x) { return new I(); } - - ReturnedFromJava foo2(T x) { - return new ReturnedFromJava(); - } + // TODO(abeln): test returning a Scala generic from Java } diff --git a/tests/pos/explicit-null-generics/S.scala b/tests/pos/explicit-null-generics/S.scala index 53d449e77c77..8c33ba3f0368 100644 --- a/tests/pos/explicit-null-generics/S.scala +++ b/tests/pos/explicit-null-generics/S.scala @@ -4,6 +4,4 @@ class S { val j = new J() // Check that the inside of a Java generic isn't nullified val i: I[String]|Null = j.foo("hello") - // ... but if the generic is Scala-defined and used from Java, then the |Null _is_ propagated to the inside. - val fromJava: ReturnedFromJava[String|Null]|Null = j.foo2("hello") } From 1a7ad573d27e5de3256833adf8a800326a2017a9 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 13 Nov 2018 13:55:56 -0500 Subject: [PATCH 056/127] Fix tests --- tests/pos/t7033.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/t7033.scala b/tests/pos/t7033.scala index a4d256673b69..301c78a467b4 100644 --- a/tests/pos/t7033.scala +++ b/tests/pos/t7033.scala @@ -9,7 +9,7 @@ class Wrap { implicit class Y[Y](val a: Y) Y[Int](0) implicit class Z[Z[_]](val a: Z[Wrap.this.Z[Z]]) - Z[List](List(new Z[List](null))) + Z[List](List(new Z[List](???))) } case class X[X](val a: X) From 62399668b55672dbdbead8999d9f17e61a32e5ad Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 14 Nov 2018 16:43:07 -0500 Subject: [PATCH 057/127] Better handling of SAM types and nullability This fixes additional cases: e.g. resolution of method overloading. It extends the current SAMType extractor to match prototypes that are nullable: e.g. `Function`[Int, Int]|Null`. --- .../src/dotty/tools/dotc/core/Types.scala | 47 +++++++++++-------- .../tools/dotc/transform/ExpandSAMs.scala | 4 +- .../src/dotty/tools/dotc/typer/Typer.scala | 17 +++---- .../pos/explicit-null-infer-lambda-arg.scala | 11 ----- tests/pos/explicit-null-sam-types.scala | 21 +++++++++ 5 files changed, 57 insertions(+), 43 deletions(-) delete mode 100644 tests/pos/explicit-null-infer-lambda-arg.scala create mode 100644 tests/pos/explicit-null-sam-types.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 2cd11ba5f426..db950db9511e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -998,18 +998,18 @@ object Types { case _ => this } - /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ - def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { - case OrType(left, right) if right.isJavaNullType => left.stripJavaNull - case _ => this - } - /** Strips the nullability from a type: `T | Null` goes to `T` */ def stripNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { case OrType(left, right) if right.isRefToNull => left.stripNull case _ => this } + /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ + def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { + case OrType(left, right) if right.isJavaNullType => left.stripJavaNull + case _ => this + } + /** Converts types of the form `Null | T` to `T | Null`. Does not do any widening or dealiasing. */ def normalizeNull(implicit ctx: Context): Type = { this match { @@ -4280,6 +4280,7 @@ object Types { case et: ExprType => true case _ => false } + if ((tp.cls is Trait) || zeroParams(tp.cls.primaryConstructor.info)) tp // !!! needs to be adapted once traits have parameters else NoType case tp: AppliedType => @@ -4295,6 +4296,7 @@ object Types { case _ => NoType } + def isInstantiatable(tp: Type)(implicit ctx: Context): Boolean = zeroParamClass(tp) match { case cinfo: ClassInfo => val selfType = cinfo.selfType.asSeenFrom(tp, cinfo.cls) @@ -4302,15 +4304,21 @@ object Types { case _ => false } - def unapply(tp: Type)(implicit ctx: Context): Option[MethodType] = - if (isInstantiatable(tp)) { - val absMems = tp.abstractTermMembers + + def unapply(tp: Type)(implicit ctx: Context): Option[MethodType] = { + // Strip the nullability from the type (if it exists) before matching + // against the SAM type. This is so that we can use e.g. `Function1[Int, Int]|Null` + // as a prototype for e.g. `(x) => x`. + // See tests/pos/explicit-null-sam-types.scala + val strippedTp = tp.stripNull + if (isInstantiatable(strippedTp)) { + val absMems = strippedTp.abstractTermMembers // println(s"absMems: ${absMems map (_.show) mkString ", "}") if (absMems.size == 1) absMems.head.info match { case mt: MethodType if !mt.isParamDependent && - !defn.isImplicitFunctionType(mt.resultType) => - val cls = tp.classSymbol + !defn.isImplicitFunctionType(mt.resultType) => + val cls = strippedTp.classSymbol // Given a SAM type such as: // @@ -4336,7 +4344,7 @@ object Types { mapOver(info.alias) case TypeBounds(lo, hi) => range(atVariance(-variance)(apply(lo)), apply(hi)) - case _ => + case _ => range(defn.NothingType, defn.AnyType) // should happen only in error cases } case _ => @@ -4348,17 +4356,18 @@ object Types { case _ => None } - else if (tp isRef defn.PartialFunctionClass) - // To maintain compatibility with 2.x, we treat PartialFunction specially, - // pretending it is a SAM type. In the future it would be better to merge - // Function and PartialFunction, have Function1 contain a isDefinedAt method - // def isDefinedAt(x: T) = true - // and overwrite that method whenever the function body is a sequence of - // case clauses. + else if (strippedTp isRef defn.PartialFunctionClass) + // To maintain compatibility with 2.x, we treat PartialFunction specially, + // pretending it is a SAM type. In the future it would be better to merge + // Function and PartialFunction, have Function1 contain a isDefinedAt method + // def isDefinedAt(x: T) = true + // and overwrite that method whenever the function body is a sequence of + // case clauses. absMems.find(_.symbol.name == nme.apply).map(_.info.asInstanceOf[MethodType]) else None } else None + } } // ----- TypeMaps -------------------------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index 8e207d31ef5f..7c28638c2ebe 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -38,10 +38,10 @@ class ExpandSAMs extends MiniPhase { tree // it's a plain function case tpe if defn.isImplicitFunctionType(tpe) => tree - case tpe @ SAMType(_) if tpe.isRef(defn.PartialFunctionClass) => + case tpe @ SAMType(_) if tpe.stripNull.isRef(defn.PartialFunctionClass) => val tpe1 = checkRefinements(tpe, fn.pos) toPartialFunction(tree, tpe1) - case tpe @ SAMType(_) if isPlatformSam(tpe.classSymbol.asClass) => + case tpe @ SAMType(_) if isPlatformSam(tpe.stripNull.classSymbol.asClass) => checkRefinements(tpe, fn.pos) tree case tpe => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index da7713b8c6fe..07a55a9288cf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -826,21 +826,16 @@ class Typer extends Namer case _ => false } - // If the prototype is of the form e.g. `Function1[Int, Int]|Null`, then we should be - // able discard the `| Null` from it, since the literal itself isn't nullable. - // This gives us a more precise (useful) prototype. - val pt1 = pt.stripNull - - pt1 match { - case pt1: TypeVar if untpd.isFunctionWithUnknownParamType(tree) => + pt match { + case pt: TypeVar if untpd.isFunctionWithUnknownParamType(tree) => // try to instantiate `pt` if this is possible. If it does not // work the error will be reported later in `inferredParam`, // when we try to infer the parameter type. - isFullyDefined(pt1, ForceDegree.noBottom) + isFullyDefined(pt, ForceDegree.noBottom) case _ => } - val (protoFormals, resultTpt) = decomposeProtoFunction(pt1, params.length) + val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length) def refersTo(arg: untpd.Tree, param: untpd.ValDef): Boolean = arg match { case Ident(name) => name == param.name @@ -921,7 +916,7 @@ class Typer extends Namer } case _ => } - errorType(AnonymousFunctionMissingParamType(param, params, tree, pt1), param.pos) + errorType(AnonymousFunctionMissingParamType(param, params, tree, pt), param.pos) } def protoFormal(i: Int): Type = @@ -952,7 +947,7 @@ class Typer extends Namer inferredParamType(param, protoFormal(i)).underlyingIfRepeated(isJava = false))) desugar.makeClosure(inferredParams, fnBody, resultTpt, isImplicit) } - typed(desugared, pt1) + typed(desugared, pt) } def typedClosure(tree: untpd.Closure, pt: Type)(implicit ctx: Context): Tree = track("typedClosure") { diff --git a/tests/pos/explicit-null-infer-lambda-arg.scala b/tests/pos/explicit-null-infer-lambda-arg.scala deleted file mode 100644 index 8b8ff1f2f40e..000000000000 --- a/tests/pos/explicit-null-infer-lambda-arg.scala +++ /dev/null @@ -1,11 +0,0 @@ -class Foo { - implicit class NN[T](x: T|Null) { - def nn: T = x.asInstanceOf[T] - } - import java.util.ArrayList, java.util.stream.{Stream => JStream} - // Test that we can infer the argument of the lambda in the application of `map`, even - // though the prototype is of the form Function[A,B]|Null - new java.util.ArrayList[String]().stream.map(_.nn.toInt).map(_.nn.toString): JStream[String|Null]|Null - - val x: (Int => Int)|Null = (y) => y -} diff --git a/tests/pos/explicit-null-sam-types.scala b/tests/pos/explicit-null-sam-types.scala new file mode 100644 index 000000000000..4cf020400747 --- /dev/null +++ b/tests/pos/explicit-null-sam-types.scala @@ -0,0 +1,21 @@ +class Foo { + import java.util.function._ + import java.util.stream._ + + // Assignment context + val p: Predicate[String]|Null = (x) => true + + // Method invocation context + val s: Stream[String] = ??? + s.filter((x) => true) + + // Method overloading context + trait MyFun { + def apply(x: Int): String + } + + def foo(m: MyFun|Null) = {} + def foo(m: Int) = {} + + foo((x) => "hello") +} From 26714fe44507980b69e416882c6b480738fd68c0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 15 Nov 2018 17:29:57 -0500 Subject: [PATCH 058/127] Ignore nulls when doing override checks When a Scala symbol overrides a Java-defined symbol, ignore `|Null` in the type during RefChecks. This is for easier migration and (in the future) so we can link against pre-null and post-null versions of a Scala library. --- .../dotty/tools/dotc/core/Denotations.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 29 ++++++++++++++++--- .../dotc/transform/ElimErasedValueType.scala | 2 +- .../tools/dotc/transform/ElimRepeated.scala | 12 ++++++-- .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- tests/neg/explicit-null-override/J.java | 6 ++++ tests/neg/explicit-null-override/S.scala | 19 ++++++++++++ tests/neg/explicit-null-varargs-src/J.java | 12 -------- tests/neg/explicit-null-varargs-src/S.scala | 14 --------- 9 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 tests/neg/explicit-null-override/J.java create mode 100644 tests/neg/explicit-null-override/S.scala delete mode 100644 tests/neg/explicit-null-varargs-src/J.java delete mode 100644 tests/neg/explicit-null-varargs-src/S.scala diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index 243d4026725a..cdaefc05bb92 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -343,7 +343,7 @@ object Denotations { * as a member of type `site`, `NoDenotation` if none exists. */ def matchingDenotation(site: Type, targetType: Type)(implicit ctx: Context): SingleDenotation = { - def qualifies(sym: Symbol) = site.memberInfo(sym).matchesLoosely(targetType) + def qualifies(sym: Symbol) = site.memberInfo(sym).matchesLoosely(targetType, ignoreNull = sym.is(JavaDefined)) if (isOverloaded) { atSignature(targetType.signature, site, relaxed = true) match { case sd: SingleDenotation => sd.matchingDenotation(site, targetType) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index db950db9511e..493e2ee84fb0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -927,12 +927,16 @@ object Types { /** This is the same as `matches` except that it also matches => T with T and * vice versa. + * Additionally, if `ignoreNull` is set, nullable unions embedded in the types will be + * removed: e.g. `String|Null` will match `String` and `C[String|Null]|Null` will match `C[String]`. */ - def matchesLoosely(that: Type)(implicit ctx: Context): Boolean = + def matchesLoosely(that: Type, ignoreNull: Boolean = false)(implicit ctx: Context): Boolean = (this matches that) || { - val thisResult = this.widenExpr - val thatResult = that.widenExpr - (this eq thisResult) != (that eq thatResult) && (thisResult matchesLoosely thatResult) + val thisResult = if (ignoreNull) this.widenExpr.stripNullStruct else this.widenExpr + val thatResult = if(ignoreNull) that.widenExpr.stripNullStruct else that.widenExpr + // TODO(abeln): was the requirement that only of the types changes just an optimization? +// (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult, ignoreNull) + ((this ne thisResult) || (that ne thatResult)) && thisResult.matchesLoosely(thatResult, ignoreNull) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ @@ -1004,6 +1008,23 @@ object Types { case _ => this } + /** Structurally strips the nullability from a type. + * e.g. `T|Null` => `T` + * `Array[T|Null]|Null` => `Array[T]` + * `(x: T|Null): String|Null` => `(x: T): String` + */ + def stripNullStruct(implicit ctx: Context): Type = { + object StripNullMap extends TypeMap { + override def apply(tp: Type): Type = tp match { + case tp: OrType => mapOver(tp).stripNull + case _ => mapOver(tp) + } + } + // TODO(abeln): can we make this a no-op if the type isn't modified? + // In particular, can we avoid widening the type? + StripNullMap(this.widenDealias) + } + /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { case OrType(left, right) if right.isJavaNullType => left.stripJavaNull diff --git a/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala b/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala index d9327f53d145..e1fa3e1265a2 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala @@ -89,7 +89,7 @@ class ElimErasedValueType extends MiniPhase with InfoTransformer { val info1 = site.memberInfo(sym1) val info2 = site.memberInfo(sym2) def isDefined(sym: Symbol) = sym.originDenotation.validFor.firstPhaseId <= ctx.phaseId - if (isDefined(sym1) && isDefined(sym2) && !info1.matchesLoosely(info2)) + if (isDefined(sym1) && isDefined(sym2) && !info1.matchesLoosely(info2, sym2.is(JavaDefined))) // The reason for the `isDefined` condition is that we need to exclude mixin forwarders // from the tests. For instance, in compileStdLib, compiling scala.immutable.SetProxy, line 29: // new AbstractSet[B] with SetProxy[B] { val self = newSelf } diff --git a/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala b/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala index 625be403a508..ef3b6833d65a 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala @@ -140,12 +140,20 @@ class ElimRepeated extends MiniPhase with InfoTransformer { thisPhase => info = toJavaVarArgs(ddef.symbol.info)).enteredAfter(thisPhase).asTerm val bridgeDef = polyDefDef(bridge, trefs => vrefss => { val (vrefs :+ varArgRef) :: vrefss1 = vrefss + // If the varargs comes from Java, it'll have a type of the form `Array[T]|JavaNull`, so to + // get the element type we need to strip away the nullability. + val arrayTpe = varArgRef.tpe.widen.stripNull + // Since we're overriding a Java varargs, the user will pass in a `Array[T]|JavaNull`. + // But our Scala-defined varargs takes in a `Seq[T]`, so we need to cast away the nullability. + // This means that if the user passes `null` to the Scala method it'll be allowed by the + // type system, but will throw at runtime. + val varArgArg = varArgRef.ensureConforms(arrayTpe) // Can't call `.argTypes` here because the underlying array type is of the // form `Array[_ <: SomeType]`, so we need `.argInfos` to get the `TypeBounds`. - val elemtp = varArgRef.tpe.widen.argInfos.head + val elemtp = arrayTpe.argInfos.head ref(original.termRef) .appliedToTypes(trefs) - .appliedToArgs(vrefs :+ tpd.wrapArray(varArgRef, elemtp)) + .appliedToArgs(vrefs :+ tpd.wrapArray(varArgArg, elemtp)) .appliedToArgss(vrefss1) }) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 1d84c3478d33..a74f6daf9692 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -749,7 +749,7 @@ object RefChecks { def hasMatchingSym(inclazz: Symbol, member: Symbol): Boolean = { def isSignatureMatch(sym: Symbol) = !sym.isTerm || - clazz.thisType.memberInfo(sym).matchesLoosely(member.info) + clazz.thisType.memberInfo(sym).matchesLoosely(member.info, inclazz.is(JavaDefined)) /* The rules for accessing members which have an access boundary are more * restrictive in java than scala. Since java has no concept of package nesting, diff --git a/tests/neg/explicit-null-override/J.java b/tests/neg/explicit-null-override/J.java new file mode 100644 index 000000000000..6ca85c44b958 --- /dev/null +++ b/tests/neg/explicit-null-override/J.java @@ -0,0 +1,6 @@ +class C {} + +class J { + void foo(String x){ } + void bar(C>> x){} +} diff --git a/tests/neg/explicit-null-override/S.scala b/tests/neg/explicit-null-override/S.scala new file mode 100644 index 000000000000..0ea4e9631456 --- /dev/null +++ b/tests/neg/explicit-null-override/S.scala @@ -0,0 +1,19 @@ +class S extends J { + override def foo(x: String): Unit = {} + override def bar(x: C[C[C[String]]]): Unit = {} +} + +class S2 extends J { + override def foo(x: String|Null): Unit = {} + override def bar(x: C[C[C[String|Null]|Null]|Null]|Null): Unit = {} +} + +class Base { + def foo(x: String): Unit = {} + def bar(x: String|Null): Unit = {} +} + +class Derived extends Base { + override def foo(x: String|Null): Unit = {} // error: can't ignore null when extending from Scala + override def bar(x: String): Unit = {} // error: can't ignore null when extending from Scala +} diff --git a/tests/neg/explicit-null-varargs-src/J.java b/tests/neg/explicit-null-varargs-src/J.java deleted file mode 100644 index 268b7f065ddf..000000000000 --- a/tests/neg/explicit-null-varargs-src/J.java +++ /dev/null @@ -1,12 +0,0 @@ -class J { - void foo(String... ss) {} -} - -class Animal {} - -class Dog extends Animal {} - -class J2 { - void foo(Animal... animal) {} -} - diff --git a/tests/neg/explicit-null-varargs-src/S.scala b/tests/neg/explicit-null-varargs-src/S.scala deleted file mode 100644 index edea919e2ca7..000000000000 --- a/tests/neg/explicit-null-varargs-src/S.scala +++ /dev/null @@ -1,14 +0,0 @@ -class S { - - val j2 = new J2() - val x: Array[Dog] = ??? - j2.foo(x: _*) - - //val j = new J() - //val x: Array[String] = ??? - //j.foo(x: _*) // error: expected Array[String|Null] but got Array[String] - - //val x2: Array[String|Null] = ??? - //j.foo(x2: _*) // ok - // j.foo(null: _*) // ok -} From 3b9ecd34f56c08130658695601f7f2dd8b1818d4 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 16 Nov 2018 18:01:01 -0500 Subject: [PATCH 059/127] Take 2 at ignoring JavaNull in override checks Instead of propagating from the outside whether we're matching against a Java-originated type, strip nulls if either of the two types has `|JavaNull` in it. --- .../dotty/tools/dotc/core/Denotations.scala | 2 +- .../src/dotty/tools/dotc/core/JavaNull.scala | 67 +++++++++++++------ .../src/dotty/tools/dotc/core/Types.scala | 41 +++++------- .../dotc/transform/ElimErasedValueType.scala | 2 +- .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- tests/neg/explicit-null-override/S.scala | 4 +- 6 files changed, 67 insertions(+), 51 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index cdaefc05bb92..243d4026725a 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -343,7 +343,7 @@ object Denotations { * as a member of type `site`, `NoDenotation` if none exists. */ def matchingDenotation(site: Type, targetType: Type)(implicit ctx: Context): SingleDenotation = { - def qualifies(sym: Symbol) = site.memberInfo(sym).matchesLoosely(targetType, ignoreNull = sym.is(JavaDefined)) + def qualifies(sym: Symbol) = site.memberInfo(sym).matchesLoosely(targetType) if (isOverloaded) { atSignature(targetType.signature, site, relaxed = true) match { case sd: SingleDenotation => sd.matchingDenotation(site, targetType) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index cbce29137cfb..d8c1bf3bce4f 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -4,7 +4,7 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags.JavaDefined import dotty.tools.dotc.core.StdNames.{jnme, nme} import dotty.tools.dotc.core.Symbols.{Symbol, defn, _} -import dotty.tools.dotc.core.Types.{AppliedType, LambdaType, MethodType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef} +import dotty.tools.dotc.core.Types.{AppliedType, LambdaType, MethodType, OrType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef} /** Transformation from Java (nullable) to Scala (non-nullable) types */ object JavaNull { @@ -54,13 +54,42 @@ object JavaNull { fromWhitelistTp } else { // Default case: nullify everything. - val nullMap = new JavaNullMap() - nullMap(tp) + nullifyType(tp) } } + /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact + * that Java types remain nullable by default. + */ + def nullifyType(tpe: Type)(implicit ctx: Context): Type = { + val loc = new JavaNullLoc({ + case tp: OrType => tp + case tp => tp.toJavaNullable + }) + loc(tpe) + } + + /** Strip any `| Null` from locations where this transform would've inserted them. If `tpe` does not + * contain any nullable unions, then the same reference will be returned. + */ + def stripNullableUnions(tpe: Type)(implicit ctx: Context): Type = { + val loc = new JavaNullLoc(_.stripNull) + loc(tpe) + } + + /** Does `tpe` contain any `| JavaNull` in locations where the transform would've inserted them? */ + def containsJavaNullableUnions(tpe: Type)(implicit ctx: Context): Boolean = { + var hasUnion = false + val loc = new JavaNullLoc(tp => { + if (tp.isJavaNullableUnion) hasUnion = true + tp + }) + loc(tpe) + hasUnion + } + /** A policy that special cases the handling of some symbol or class of symbols. */ - sealed trait NullifyPolicy { + private sealed trait NullifyPolicy { /** Whether the policy applies to `sym`. */ def isApplicable(sym: Symbol): Boolean /** Nullifies `tp` according to the policy. Should call `isApplicable` first. */ @@ -68,7 +97,7 @@ object JavaNull { } /** A policy that avoids modifying a field. */ - case class FieldP(trigger: Symbol => Boolean)(implicit ctx: Context) extends NullifyPolicy { + private case class FieldP(trigger: Symbol => Boolean)(implicit ctx: Context) extends NullifyPolicy { override def isApplicable(sym: Symbol): Boolean = trigger(sym) override def apply(tp: Type): Type = { assert(!tp.isJavaMethod, s"FieldPolicy applies to method (non-field) type ${tp.show}") @@ -77,7 +106,7 @@ object JavaNull { } /** A policy for handling a method or poly. Can indicate whether the argument or return types should be nullified. */ - case class MethodP(trigger: Symbol => Boolean, + private case class MethodP(trigger: Symbol => Boolean, nlfyParams: Boolean = false, nlfyRes: Boolean = false) (implicit ctx: Context) extends TypeMap with NullifyPolicy { @@ -88,33 +117,27 @@ object JavaNull { case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => - val nullMap = new JavaNullMap() - val paramTpes = if (nlfyParams) mtp.paramInfos.mapConserve(nullMap) else mtp.paramInfos - val resTpe = if (nlfyRes) nullMap(mtp.resType) else mtp.resType + val paramTpes = if (nlfyParams) mtp.paramInfos.mapConserve(nullifyType) else mtp.paramInfos + val resTpe = if (nlfyRes) nullifyType(mtp.resType) else mtp.resType derivedLambdaType(mtp)(paramTpes, resTpe) } } } /** A policy that nullifies only method parameters (but not result types). */ - def paramsOnlyP(trigger: Symbol => Boolean)(implicit ctx: Context): MethodP = { + private def paramsOnlyP(trigger: Symbol => Boolean)(implicit ctx: Context): MethodP = { MethodP(trigger, nlfyParams = true, nlfyRes = false) } /** A wrapper policy that works as `inner` but additionally verifies that the symbol is contained in `owner`. */ - case class WithinSym(inner: NullifyPolicy, owner: Symbol)(implicit ctx: Context) extends NullifyPolicy { + private case class WithinSym(inner: NullifyPolicy, owner: Symbol)(implicit ctx: Context) extends NullifyPolicy { override def isApplicable(sym: Symbol): Boolean = sym.owner == owner && inner.isApplicable(sym) override def apply(tp: Type): Type = inner(tp) } - /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact - * that Java types remain nullable by default. - * - * nullify(T) = T | JavaNull if T is a type parameter or class or interface - * nullify(C[S]) = C[nullify(S)] | JavaNull if C is a generic class - */ - class JavaNullMap(implicit ctx: Context) extends TypeMap { + /** A type map that applies `op` just to the places where "|JavaNull" could potentially be added. */ + private class JavaNullLoc(op: Type => Type)(implicit ctx: Context) extends TypeMap { def shouldNullify(tp: Type): Boolean = { tp match { case tp: TypeRef => @@ -145,12 +168,14 @@ object JavaNull { case tp: TypeAlias => mapOver(tp) case tp: TypeRef if shouldNullify(tp) => - tp.toJavaNullable + op(tp) case tp: TypeParamRef => - tp.toJavaNullable + op(tp) + case tp: OrType => + op(tp) case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => val targs2 = if (shouldDescend(appTp)) targs map this else targs - AppliedType(tycons, targs2).toJavaNullable + op(derivedAppliedType(appTp, tycons, targs2)) case _ => tp } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 493e2ee84fb0..52957376e55a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -925,18 +925,24 @@ object Types { ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) } - /** This is the same as `matches` except that it also matches => T with T and - * vice versa. - * Additionally, if `ignoreNull` is set, nullable unions embedded in the types will be - * removed: e.g. `String|Null` will match `String` and `C[String|Null]|Null` will match `C[String]`. + /** This is the same as `matches` except that + * (1) it also matches => T with T and vice versa + * (2) it ignores "| JavaNull" unions embedded in the types */ - def matchesLoosely(that: Type, ignoreNull: Boolean = false)(implicit ctx: Context): Boolean = + def matchesLoosely(that: Type)(implicit ctx: Context): Boolean = (this matches that) || { - val thisResult = if (ignoreNull) this.widenExpr.stripNullStruct else this.widenExpr - val thatResult = if(ignoreNull) that.widenExpr.stripNullStruct else that.widenExpr + var thisResult = this.widenExpr + var thatResult = that.widenExpr + // If either of the types contains a `| JavaNull` union, then we want to get right of _all_ nullable + // unions that appear in places where Java code could have a `JavaNull`. + // e.g. we want `String|Null => String` to `String|JavaNull => String|JavaNull`. + if (JavaNull.containsJavaNullableUnions(thisResult) || JavaNull.containsJavaNullableUnions(thatResult)) { + thisResult = JavaNull.stripNullableUnions(thisResult) + thatResult = JavaNull.stripNullableUnions(thatResult) + } // TODO(abeln): was the requirement that only of the types changes just an optimization? -// (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult, ignoreNull) - ((this ne thisResult) || (that ne thatResult)) && thisResult.matchesLoosely(thatResult, ignoreNull) + // (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) + ((this ne thisResult) || (that ne thatResult)) && thisResult.matchesLoosely(thatResult) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ @@ -1008,23 +1014,6 @@ object Types { case _ => this } - /** Structurally strips the nullability from a type. - * e.g. `T|Null` => `T` - * `Array[T|Null]|Null` => `Array[T]` - * `(x: T|Null): String|Null` => `(x: T): String` - */ - def stripNullStruct(implicit ctx: Context): Type = { - object StripNullMap extends TypeMap { - override def apply(tp: Type): Type = tp match { - case tp: OrType => mapOver(tp).stripNull - case _ => mapOver(tp) - } - } - // TODO(abeln): can we make this a no-op if the type isn't modified? - // In particular, can we avoid widening the type? - StripNullMap(this.widenDealias) - } - /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { case OrType(left, right) if right.isJavaNullType => left.stripJavaNull diff --git a/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala b/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala index e1fa3e1265a2..d9327f53d145 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimErasedValueType.scala @@ -89,7 +89,7 @@ class ElimErasedValueType extends MiniPhase with InfoTransformer { val info1 = site.memberInfo(sym1) val info2 = site.memberInfo(sym2) def isDefined(sym: Symbol) = sym.originDenotation.validFor.firstPhaseId <= ctx.phaseId - if (isDefined(sym1) && isDefined(sym2) && !info1.matchesLoosely(info2, sym2.is(JavaDefined))) + if (isDefined(sym1) && isDefined(sym2) && !info1.matchesLoosely(info2)) // The reason for the `isDefined` condition is that we need to exclude mixin forwarders // from the tests. For instance, in compileStdLib, compiling scala.immutable.SetProxy, line 29: // new AbstractSet[B] with SetProxy[B] { val self = newSelf } diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index a74f6daf9692..1d84c3478d33 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -749,7 +749,7 @@ object RefChecks { def hasMatchingSym(inclazz: Symbol, member: Symbol): Boolean = { def isSignatureMatch(sym: Symbol) = !sym.isTerm || - clazz.thisType.memberInfo(sym).matchesLoosely(member.info, inclazz.is(JavaDefined)) + clazz.thisType.memberInfo(sym).matchesLoosely(member.info) /* The rules for accessing members which have an access boundary are more * restrictive in java than scala. Since java has no concept of package nesting, diff --git a/tests/neg/explicit-null-override/S.scala b/tests/neg/explicit-null-override/S.scala index 0ea4e9631456..ce6d84077654 100644 --- a/tests/neg/explicit-null-override/S.scala +++ b/tests/neg/explicit-null-override/S.scala @@ -1,3 +1,5 @@ +// Check that `|JavaNull` is ignored in override checks + class S extends J { override def foo(x: String): Unit = {} override def bar(x: C[C[C[String]]]): Unit = {} @@ -5,7 +7,7 @@ class S extends J { class S2 extends J { override def foo(x: String|Null): Unit = {} - override def bar(x: C[C[C[String|Null]|Null]|Null]|Null): Unit = {} + override def bar(x: C[C[C[String]]]|Null): Unit = {} } class Base { From 02f8bd4f697180d2b9eab57695b94aa700acf5f8 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 16 Nov 2018 18:04:28 -0500 Subject: [PATCH 060/127] Add additional case to override test --- tests/neg/explicit-null-override/S.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/neg/explicit-null-override/S.scala b/tests/neg/explicit-null-override/S.scala index ce6d84077654..5f2371ee0f7b 100644 --- a/tests/neg/explicit-null-override/S.scala +++ b/tests/neg/explicit-null-override/S.scala @@ -10,6 +10,10 @@ class S2 extends J { override def bar(x: C[C[C[String]]]|Null): Unit = {} } +class S3 extends J { + override def bar(x: C[C[C[String|Null]]]): Unit = {} // error: since the null transform doesn't add nulls in the inside, neither should the Scala user +} + class Base { def foo(x: String): Unit = {} def bar(x: String|Null): Unit = {} From b0dc6b1d8b57b5805c12778fe3dfec4567abc0a4 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 22 Nov 2018 16:46:40 -0500 Subject: [PATCH 061/127] Add .nn extension method to strip away nullability --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 3 ++- library/src-scala3/scala/NonNull.scala | 7 +++++++ tests/pos/explicit-null-nn/J.java | 4 ++++ tests/pos/explicit-null-nn/S.scala | 7 +++++++ tests/pos/i1044.scala | 6 +----- tests/pos/i801.scala | 4 ---- 6 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 library/src-scala3/scala/NonNull.scala create mode 100644 tests/pos/explicit-null-nn/J.java create mode 100644 tests/pos/explicit-null-nn/S.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6c34635e6d7e..62417d209d7d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1069,7 +1069,8 @@ class Definitions { val PredefImportFns: List[() => TermRef] = List[() => TermRef]( () => ScalaPredefModuleRef, - () => DottyPredefModuleRef + () => DottyPredefModuleRef, + () => ctx.requiredModuleRef("scala.NonNull") // TODO(abeln): move to right place ) lazy val RootImportFns: List[() => TermRef] = diff --git a/library/src-scala3/scala/NonNull.scala b/library/src-scala3/scala/NonNull.scala new file mode 100644 index 000000000000..db9c95a3e40a --- /dev/null +++ b/library/src-scala3/scala/NonNull.scala @@ -0,0 +1,7 @@ +package scala + +object NonNull { + implicit class NonNull[T](x: T|Null) { + def nn: T = x.asInstanceOf[T] + } +} diff --git a/tests/pos/explicit-null-nn/J.java b/tests/pos/explicit-null-nn/J.java new file mode 100644 index 000000000000..96ac77a528f5 --- /dev/null +++ b/tests/pos/explicit-null-nn/J.java @@ -0,0 +1,4 @@ +class J { + String foo() { return "hello"; } + String[] bar() { return null; } +} diff --git a/tests/pos/explicit-null-nn/S.scala b/tests/pos/explicit-null-nn/S.scala new file mode 100644 index 000000000000..85e6f4581882 --- /dev/null +++ b/tests/pos/explicit-null-nn/S.scala @@ -0,0 +1,7 @@ +class S { + val j = new J() + // Test that the `nn` extension method can be used to strip away + // nullability from a type. + val s: String = j.foo.nn + val a: Array[String|Null] = j.bar.nn +} diff --git a/tests/pos/i1044.scala b/tests/pos/i1044.scala index 8f8619c23f4d..97f024b03a8d 100644 --- a/tests/pos/i1044.scala +++ b/tests/pos/i1044.scala @@ -1,7 +1,3 @@ object Test { - implicit class NN[T](x: T|Null) { - def nn: T = x.asInstanceOf[T] - } - - val x = ???.getClass.getMethods.nn.head.nn.getParameterTypes.nn.mkString(",") + val x = ???.getClass.getMethods.head.nn.getParameterTypes.nn.mkString(",") } diff --git a/tests/pos/i801.scala b/tests/pos/i801.scala index 3f6865f9a90e..af3563d74953 100644 --- a/tests/pos/i801.scala +++ b/tests/pos/i801.scala @@ -1,8 +1,4 @@ class Foo { - implicit class NN[T](x: T|Null) { - def nn: T = x.asInstanceOf[T] - } - object T1 { import java.util.ArrayList, java.util.stream.{Stream => JStream} new java.util.ArrayList[String]().stream.map(_.nn.toInt).map(_.nn.toString): JStream[String|Null]|Null From 78556d71c48078475e5b3346b6f5b0c772dcf98f Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 22 Nov 2018 16:59:55 -0500 Subject: [PATCH 062/127] Strip away JavaNull during implicit conversion search When looking for an implicit conversion from A|JavaNull -> B, if we can't find it, then also look for an implicit conversion from A -> B. However, only strip `JavaNull` away, but not `Null`. --- .../src/dotty/tools/dotc/typer/Implicits.scala | 11 +++++++++-- tests/neg/explicit-null-implicits/J.java | 7 +++++++ tests/neg/explicit-null-implicits/S.scala | 16 ++++++++++++++++ tests/run/unit-val.scala | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/neg/explicit-null-implicits/J.java create mode 100644 tests/neg/explicit-null-implicits/S.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 25f4a396f2fd..60158594f82c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -604,8 +604,15 @@ trait Implicits { self: Typer => SelectionProto(name, memberProto, compat, privateOK = false) case tp => tp } - try inferImplicit(adjust(to), from, from.pos) - catch { + try { + val to1 = adjust(to) + inferImplicit(to1, from, from.pos) match { + case err: SearchFailure if !err.isAmbiguous && from.tpe.isJavaNullableUnion => + val from1 = from.ensureConforms(from.tpe.stripJavaNull) + inferImplicit(to1, from1, from.pos) + case res => res + } + } catch { case ex: AssertionError => implicits.println(s"view $from ==> $to") implicits.println(ctx.typerState.constraint.show) diff --git a/tests/neg/explicit-null-implicits/J.java b/tests/neg/explicit-null-implicits/J.java new file mode 100644 index 000000000000..0f6778071f81 --- /dev/null +++ b/tests/neg/explicit-null-implicits/J.java @@ -0,0 +1,7 @@ + +class J { + Cat getCat() { return null; } +} + +class Cat {} +class Dog {} diff --git a/tests/neg/explicit-null-implicits/S.scala b/tests/neg/explicit-null-implicits/S.scala new file mode 100644 index 000000000000..7d46c1a1cdc5 --- /dev/null +++ b/tests/neg/explicit-null-implicits/S.scala @@ -0,0 +1,16 @@ + +class S { + implicit def cat2Dog(cat: Cat): Dog = ??? + + val j = new J() + // j.getCat() returns a Cat|JavaNull, so we look for implicit conversions + // from Cat to Dog + bark(j.getCat()) + + // We can't find the implicit conversion, because `Null` (as opposed to `JavaNull`) + // isn't stripped. + def getCat2: Cat|Null = null + bark(getCat2) // error + + def bark(dog: Dog) = {} +} diff --git a/tests/run/unit-val.scala b/tests/run/unit-val.scala index 58175d659ff8..d3b1bb2d5dab 100644 --- a/tests/run/unit-val.scala +++ b/tests/run/unit-val.scala @@ -5,7 +5,7 @@ object Test { println(1) f.foo f.foo - assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } } From 89b920b65a86244fc1969b20330f875c34872b76 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 22 Nov 2018 17:22:36 -0500 Subject: [PATCH 063/127] Fix a few tests --- tests/generic-java-signatures/i3411.scala | 4 ++-- tests/generic-java-signatures/lowerBoundClass.scala | 2 +- tests/neg-tailcall/tailrec-2.scala | 4 ++-- tests/neg/t5729.scala | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/generic-java-signatures/i3411.scala b/tests/generic-java-signatures/i3411.scala index b8c4b9f47a98..acbadb6aecb0 100644 --- a/tests/generic-java-signatures/i3411.scala +++ b/tests/generic-java-signatures/i3411.scala @@ -4,7 +4,7 @@ object Foo { object Test { def main(args: Array[String]): Unit = { - val f1 = Foo.getClass.getMethods.find(_.getName.endsWith("foo")).get - println(f1.toGenericString) + val f1 = Foo.getClass.getMethods.find(_.nn.getName.endsWith("foo")).get + println(f1.nn.toGenericString) } } diff --git a/tests/generic-java-signatures/lowerBoundClass.scala b/tests/generic-java-signatures/lowerBoundClass.scala index 44a0e9cf7f97..660b983d1d77 100644 --- a/tests/generic-java-signatures/lowerBoundClass.scala +++ b/tests/generic-java-signatures/lowerBoundClass.scala @@ -3,7 +3,7 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + println(tp.nn.getName + " <: " + tp.nn.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/neg-tailcall/tailrec-2.scala b/tests/neg-tailcall/tailrec-2.scala index b6683251f0b0..bb67cf351dbb 100644 --- a/tests/neg-tailcall/tailrec-2.scala +++ b/tests/neg-tailcall/tailrec-2.scala @@ -8,13 +8,13 @@ sealed abstract class Super[+A] { class Bop1[+A](val element: A) extends Super[A] { @tailrec final def f[B >: A](mem: List[B]): List[B] = - (null: Super[A]).f(mem) // error: recursive call targeting a supertype + (??? : Super[A]).f(mem) // error: recursive call targeting a supertype @tailrec final def f1[B >: A](mem: List[B]): List[B] = this.g(mem) // error: TailRec optimisation not applicable } // These succeed class Bop2[+A](val element: A) extends Super[A] { - @tailrec final def f[B >: A](mem: List[B]): List[B] = (null: Bop2[A]).f(mem) + @tailrec final def f[B >: A](mem: List[B]): List[B] = (??? : Bop2[A]).f(mem) } object Bop3 extends Super[Nothing] { @tailrec final def f[B](mem: List[B]): List[B] = (??? : Bop3.type).f(mem) diff --git a/tests/neg/t5729.scala b/tests/neg/t5729.scala index 69df11fcbbe0..a6f467798de2 100644 --- a/tests/neg/t5729.scala +++ b/tests/neg/t5729.scala @@ -2,7 +2,7 @@ trait T[X] object Test { def join(in: Seq[T[_]]): Int = ??? def join[S](in: Seq[T[S]]): String = ??? - join(null: Seq[T[_]]) // error: ambiguous + join(??? : Seq[T[_]]) // error: ambiguous } object C { From ccdba0e524a52b47842699b256b0ab9a29494571 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 23 Nov 2018 10:36:27 -0500 Subject: [PATCH 064/127] Moar tests --- tests/neg/i1907.scala | 4 ++-- tests/neg/tryPatternMatchEq.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/neg/i1907.scala b/tests/neg/i1907.scala index 6bc3bb56f7c4..e240b64a9090 100644 --- a/tests/neg/i1907.scala +++ b/tests/neg/i1907.scala @@ -2,6 +2,6 @@ import java.io.File object Test { Some(new File(".")) - .map(_.listFiles).getOrElse(Array.empty) // error: undetermined ClassTag - .map(_.listFiles) + .map(_.listFiles.nn).getOrElse(Array.empty) // error: undetermined ClassTag + .map(_.nn.listFiles) } diff --git a/tests/neg/tryPatternMatchEq.scala b/tests/neg/tryPatternMatchEq.scala index d29924d63dd9..17a083818998 100644 --- a/tests/neg/tryPatternMatchEq.scala +++ b/tests/neg/tryPatternMatchEq.scala @@ -4,7 +4,7 @@ import java.lang.IllegalArgumentException object IAE { def unapply(e: Exception): Option[String] = - if (e.isInstanceOf[IllegalArgumentException]) Some(e.getMessage) + if (e.isInstanceOf[IllegalArgumentException]) Some(e.getMessage.nn) else None } From cae4ef5275acd47b2a11deedbb4f15c016ca08d0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 23 Nov 2018 10:57:57 -0500 Subject: [PATCH 065/127] Don't use null literal in parser error terms --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 9 ++++++--- .../src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 09c943697950..98aea0696937 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -317,7 +317,10 @@ object Parsers { accept(SEMI) } - def errorTermTree: Literal = atPos(in.offset) { Literal(Constant(null)) } + /** An error tree that stands for a pattern or an expression */ + def errorTermTree(inPattern: Boolean): Tree = atPos(in.offset) { + Ident(if (inPattern) nme.WILDCARD else nme.???) + } private[this] var inFunReturnType = false private def fromWithinReturnType[T](body: => T): T = { @@ -1457,7 +1460,7 @@ object Parsers { if (isLiteral) literal() else { syntaxErrorOrIncomplete(IllegalStartSimpleExpr(tokenString(in.token))) - errorTermTree + errorTermTree(inPattern = false) } } simpleExprRest(t, canApply) @@ -1749,7 +1752,7 @@ object Parsers { if (isLiteral) literal(inPattern = true) else { syntaxErrorOrIncomplete(IllegalStartOfSimplePattern()) - errorTermTree + errorTermTree(inPattern = true) } } diff --git a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala index 0f0a0d36bf53..fb999ccf383f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala @@ -130,7 +130,7 @@ object MarkupParsers { try handle.parseAttribute(Position(start, curOffset, mid), tmp) catch { case e: RuntimeException => - errorAndResult("error parsing attribute value", parser.errorTermTree) + errorAndResult("error parsing attribute value", parser.errorTermTree(inPattern = false)) } case '{' => @@ -330,7 +330,7 @@ object MarkupParsers { finally parser.in resume Tokens.XMLSTART if (output == null) - parser.errorTermTree + parser.errorTermTree(inPattern = false) else output } From 17b26ae522260791f2d859922a56201b80d3f6af Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 23 Nov 2018 11:06:44 -0500 Subject: [PATCH 066/127] A few more --- tests/neg-custom-args/isInstanceOf/t2755.scala | 4 ++-- tests/neg/i2378.scala | 2 +- tests/neg/i2960.scala | 2 +- tests/neg/patmat2.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/neg-custom-args/isInstanceOf/t2755.scala b/tests/neg-custom-args/isInstanceOf/t2755.scala index 9073e9253098..bcf261f6d63d 100644 --- a/tests/neg-custom-args/isInstanceOf/t2755.scala +++ b/tests/neg-custom-args/isInstanceOf/t2755.scala @@ -10,7 +10,7 @@ object Test { case x: Array[_] => 6 case _ => 7 } - def f2(a: Array[_]) = a match { + def f2(a: Array[_]|Null) = a match { case x: Array[Int] => x(0) case x: Array[Double] => 2 case x: Array[Float] => x.sum.toInt @@ -19,7 +19,7 @@ object Test { case x: Array[_] => 6 case _ => 7 // error: only null is matched } - def f3[T](a: Array[T]) = a match { + def f3[T](a: Array[T]|Null) = a match { case x: Array[Int] => x(0) case x: Array[Double] => 2 case x: Array[Float] => x.sum.toInt diff --git a/tests/neg/i2378.scala b/tests/neg/i2378.scala index 1ec5c3c648a8..6eb66bf17e51 100644 --- a/tests/neg/i2378.scala +++ b/tests/neg/i2378.scala @@ -18,7 +18,7 @@ trait Toolbox { class Test(val tb: Toolbox) { import tb._ - implicit val cap: Cap = null + implicit val cap: Cap = ??? def foo(tree: Tree): Int = (tree: Any) match { case tb.Apply(fun, args) => 3 // error, but error message is wrong diff --git a/tests/neg/i2960.scala b/tests/neg/i2960.scala index 3a423612cb19..7914633800b6 100644 --- a/tests/neg/i2960.scala +++ b/tests/neg/i2960.scala @@ -22,7 +22,7 @@ class Tag(val name: String, this } - def apply[U](f: implicit Tag => U)(implicit t: Tag = null): this.type = { + def apply[U](f: implicit Tag => U)(implicit t: Tag|Null = null): this.type = { if(t != null) t.children += this f(this) this diff --git a/tests/neg/patmat2.scala b/tests/neg/patmat2.scala index 6b7b2c3addbe..e3c940594ad3 100644 --- a/tests/neg/patmat2.scala +++ b/tests/neg/patmat2.scala @@ -4,7 +4,7 @@ import java.lang.IllegalArgumentException object IAE { def unapply(e: Exception): Option[String] = - if (e.isInstanceOf[IllegalArgumentException]) Some(e.getMessage) + if (e.isInstanceOf[IllegalArgumentException]) Some(e.getMessage.nn) else None } From 822522ac0737929b68e8915d36693e7f3ba07d12 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 26 Nov 2018 10:15:16 -0500 Subject: [PATCH 067/127] Fix tests --- tests/neg/opaque-immutable-array.scala | 2 +- tests/neg/t7278.scala | 2 +- tests/pos/explicit-null-implicit-arg.scala | 17 +++++++++++++++++ tests/run/MeterCaseClass.scala | 2 +- tests/run/forwarder.scala | 2 +- tests/run/i2738.scala | 4 ++-- tests/run/i3396.scala | 2 +- tests/run/null-hash.scala | 8 ++++---- tests/run/t4827.scala | 4 ++-- tests/run/t6928-run.scala | 2 +- tests/run/t8188.scala | 2 +- tests/run/t8197b.scala | 2 +- tests/run/t8346.scala | 2 +- 13 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 tests/pos/explicit-null-implicit-arg.scala diff --git a/tests/neg/opaque-immutable-array.scala b/tests/neg/opaque-immutable-array.scala index 5677b0fa9ebb..995b0a1c9b56 100644 --- a/tests/neg/opaque-immutable-array.scala +++ b/tests/neg/opaque-immutable-array.scala @@ -11,7 +11,7 @@ object ia { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, ia.length) + val arr = Arrays.copyOf(ia, ia.length).nn scala.util.Sorting.quickSort(arr) arr } diff --git a/tests/neg/t7278.scala b/tests/neg/t7278.scala index 7b13535f0a60..831df135fa4e 100644 --- a/tests/neg/t7278.scala +++ b/tests/neg/t7278.scala @@ -13,7 +13,7 @@ object Test { def fail1(): Unit = { val b = new B - var x1: EE[A] = null // error: Type argument A does not conform to upper bound EC + var x1: EE[A] = ??? // error: Type argument A does not conform to upper bound EC var x2: EE[B] = new b.E // error: Type argument B does not conform to upper bound EC // x1 = x2 // gives a prior type error: B#E, required: A#E, masked to get at the real thing. } diff --git a/tests/pos/explicit-null-implicit-arg.scala b/tests/pos/explicit-null-implicit-arg.scala new file mode 100644 index 000000000000..d866cdd67c86 --- /dev/null +++ b/tests/pos/explicit-null-implicit-arg.scala @@ -0,0 +1,17 @@ + +class Foo { + + // Test that the compiler can fill in an implicit argument + // of the form `T|Null`. + def foo(implicit s: String|Null) = s + + { + implicit val arg1: String = "hello" + foo + } + + { + implicit val arg2: Null = null + foo + } +} diff --git a/tests/run/MeterCaseClass.scala b/tests/run/MeterCaseClass.scala index 1dcd9f34463a..d2112580d0ab 100644 --- a/tests/run/MeterCaseClass.scala +++ b/tests/run/MeterCaseClass.scala @@ -7,7 +7,7 @@ package a { case class Meter(underlying: Double) extends AnyVal with _root_.b.Printable { def + (other: Meter): Meter = new Meter(this.underlying + other.underlying) - def / (other: Meter)(implicit dummy: Meter.MeterArg = null): Double = this.underlying / other.underlying + def / (other: Meter)(implicit dummy: Meter.MeterArg|Null = null): Double = this.underlying / other.underlying def / (factor: Double): Meter = new Meter(this.underlying / factor) def < (other: Meter): Boolean = this.underlying < other.underlying def toFoot: Foot = new Foot(this.underlying * 0.3048) diff --git a/tests/run/forwarder.scala b/tests/run/forwarder.scala index 540acb76a58b..83e9638050d1 100644 --- a/tests/run/forwarder.scala +++ b/tests/run/forwarder.scala @@ -14,7 +14,7 @@ object Test { def main(args: Array[String]): Unit = { println( classOf[Foo].getMethods - .filter(m => (m.getModifiers & java.lang.reflect.Modifier.STATIC) != 0) + .filter(m => (m.nn.getModifiers & java.lang.reflect.Modifier.STATIC) != 0) .mkString("\n")) } } diff --git a/tests/run/i2738.scala b/tests/run/i2738.scala index 5439e9e53f37..51a32939abe8 100644 --- a/tests/run/i2738.scala +++ b/tests/run/i2738.scala @@ -28,7 +28,7 @@ object Test { qux.toString() } - def thisMethodName() = Thread.currentThread().getStackTrace()(2).getMethodName + def thisMethodName() = Thread.currentThread().getStackTrace()(2).getMethodName.nn - def thisMethodsClassName() = Thread.currentThread().getStackTrace()(2).getClassName + def thisMethodsClassName() = Thread.currentThread().getStackTrace()(2).getClassName.nn } diff --git a/tests/run/i3396.scala b/tests/run/i3396.scala index fec007591d7a..4ccdbd865824 100644 --- a/tests/run/i3396.scala +++ b/tests/run/i3396.scala @@ -14,7 +14,7 @@ object Test { def main(args: Array[String]): Unit = { - implicit val taggedInt: Tagged[Int] = null + implicit val taggedInt: Tagged[Int] = new Tagged[Int] {} assert(implicitly[Foo[Int]].value) // fooDefault diff --git a/tests/run/null-hash.scala b/tests/run/null-hash.scala index 9b1f28b083c1..961ee64434dd 100644 --- a/tests/run/null-hash.scala +++ b/tests/run/null-hash.scala @@ -1,15 +1,15 @@ object Test { - def f1 = List(5, 10, null: String).## + def f1 = List(5, 10, null: String|Null).## def f2(x: Any) = x.## - def f3 = ((55, "abc", null: List[Int])).## + def f3 = ((55, "abc", null: List[Int]|Null)).## def main(args: Array[String]): Unit = { f1 f2(null) - f2(null: String) + f2(null: String|Null) f3 null.## (null: Any).## - (null: String).## + (null: String|Null).## } } diff --git a/tests/run/t4827.scala b/tests/run/t4827.scala index 7270cf169def..d4eef0507714 100644 --- a/tests/run/t4827.scala +++ b/tests/run/t4827.scala @@ -3,7 +3,7 @@ object Test { } trait CommonTrait { - def foo(): String = null + def foo(): String = "hello" } class Foo @@ -11,5 +11,5 @@ class Foo object Foo { def goo() = new Foo() with CommonTrait - def foo(): String = null + def foo(): String = "world" } diff --git a/tests/run/t6928-run.scala b/tests/run/t6928-run.scala index 87a8884d60ec..b6046f51046e 100644 --- a/tests/run/t6928-run.scala +++ b/tests/run/t6928-run.scala @@ -1,4 +1,4 @@ -abstract class A( val someAs: A* ) { +abstract class A( val someAs: A|Null* ) { override def toString = someAs.length + " As" } object B extends A(null, null, null) diff --git a/tests/run/t8188.scala b/tests/run/t8188.scala index 9ee542f2ae1a..20d12f0bc5c3 100644 --- a/tests/run/t8188.scala +++ b/tests/run/t8188.scala @@ -9,7 +9,7 @@ object Test { def ser[T](o: T): Array[Byte] = { val baos = new ByteArrayOutputStream() new ObjectOutputStream(baos).writeObject(o) - baos.toByteArray() + baos.toByteArray().nn } def deser[T](bs: Array[Byte]): T = diff --git a/tests/run/t8197b.scala b/tests/run/t8197b.scala index a9012ddf9de4..61d588a2f2ee 100644 --- a/tests/run/t8197b.scala +++ b/tests/run/t8197b.scala @@ -1,6 +1,6 @@ object O { def foo[T](t: T) = 0 - def foo(s: String)(implicit i: DummyImplicit = null) = 1 + def foo(s: String)(implicit i: DummyImplicit|Null = null) = 1 } object Test extends dotty.runtime.LegacyApp { diff --git a/tests/run/t8346.scala b/tests/run/t8346.scala index 1c34ab35f749..9089cef9f86d 100644 --- a/tests/run/t8346.scala +++ b/tests/run/t8346.scala @@ -7,7 +7,7 @@ object Test extends dotty.runtime.LegacyApp { def sctor[A <: Set[Int]](f: Int => A)(implicit A: ClassTag[A]) : (String, Int => Set[Int]) = - (A.runtimeClass.getSimpleName, f) + (A.runtimeClass.getSimpleName.nn, f) val inits: Seq[(String, Int => Set[Int])] = { import collection.immutable.{Seq => _, _} From 887d4c010f81de235979ea33a32c599ccca4f605 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 26 Nov 2018 10:28:51 -0500 Subject: [PATCH 068/127] One more --- tests/run/Meter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/Meter.scala b/tests/run/Meter.scala index d070b7668724..4d041439b545 100644 --- a/tests/run/Meter.scala +++ b/tests/run/Meter.scala @@ -7,7 +7,7 @@ package a { class Meter(val underlying: Double) extends AnyVal with _root_.b.Printable { def + (other: Meter): Meter = new Meter(this.underlying + other.underlying) - def / (other: Meter)(implicit dummy: Meter.MeterArg = null): Double = this.underlying / other.underlying + def / (other: Meter)(implicit dummy: Meter.MeterArg|Null = null): Double = this.underlying / other.underlying def / (factor: Double): Meter = new Meter(this.underlying / factor) def < (other: Meter): Boolean = this.underlying < other.underlying def toFoot: Foot = new Foot(this.underlying * 0.3048) From 47b51c03d4a00e83643c7f7a234508c88a9d128c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 26 Nov 2018 15:51:45 -0500 Subject: [PATCH 069/127] Make it clear that we disallow comparing null against value types --- tests/neg/equality.scala | 7 ++++++- tests/neg/explicit-null-eq.scala | 26 ++++++++++++++++++++++++++ tests/pos/explicit-null-eq.scala | 9 --------- 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 tests/neg/explicit-null-eq.scala delete mode 100644 tests/pos/explicit-null-eq.scala diff --git a/tests/neg/equality.scala b/tests/neg/equality.scala index 595d1de6564f..0fdc8910e94b 100644 --- a/tests/neg/equality.scala +++ b/tests/neg/equality.scala @@ -58,11 +58,16 @@ object equality { 1 == true // error + /* null == true // OK by eqProxy or eqJBoolSBool true == null // OK by eqSBoolJBool null == 1 // OK by eqProxy or eqNumInt 1 == null // OK by eqIntNum - + */ + null == true // error + true == null // error + null == 1 // error + 1 == null // error class Fruit diff --git a/tests/neg/explicit-null-eq.scala b/tests/neg/explicit-null-eq.scala new file mode 100644 index 000000000000..970f4ec811a8 --- /dev/null +++ b/tests/neg/explicit-null-eq.scala @@ -0,0 +1,26 @@ +// Test what can be compared for equality against null. +class Foo { + // Null itself + val x0: Null = null + x0 == null + null == x0 + null == null + + // Nullable types: OK + val x1: String|Null = null + x1 == null + null == x1 + + // Reference types, even non-nullable ones: OK. + // Allowed as an escape hatch. + val x2: String = "hello" + x2 != null + x2 == null + null == x2 + + // Value types: not allowed. + 1 == null // error + null == 1 // error + true == null // error + null == true // error +} diff --git a/tests/pos/explicit-null-eq.scala b/tests/pos/explicit-null-eq.scala deleted file mode 100644 index 883e557bb2bd..000000000000 --- a/tests/pos/explicit-null-eq.scala +++ /dev/null @@ -1,9 +0,0 @@ -// Test that non-nullable types can be still be compared -// for equality against null. -class Foo { - val x: String = "hello" - if (x != null) { // allowed as escape hatch - } - if (x == null) { - } -} From 1fa226c42a8b0eb7c047b0e6becf0a115f4cffe5 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 26 Nov 2018 16:00:42 -0500 Subject: [PATCH 070/127] 3 more tests --- tests/run/hashset.scala | 2 +- tests/run/is-valid-num.scala | 2 +- tests/run/t1987.scala | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/run/hashset.scala b/tests/run/hashset.scala index a6deb704283e..1eb5bad623a3 100644 --- a/tests/run/hashset.scala +++ b/tests/run/hashset.scala @@ -26,7 +26,7 @@ object Test extends dotty.runtime.LegacyApp { println() println("*** " + creator.hashSetType + " Strings with null") - val h2 = creator.create[String] + val h2 = creator.create[String|Null] h2 += null for (i <- 0 until 20) h2 += "" + i println("null " + (h2 contains null)) diff --git a/tests/run/is-valid-num.scala b/tests/run/is-valid-num.scala index 5a4a0503da59..8d16fd692832 100644 --- a/tests/run/is-valid-num.scala +++ b/tests/run/is-valid-num.scala @@ -203,7 +203,7 @@ object Test { } def checkBigInt(bi: BigInt): Unit = { - val bd = BigDecimal(bi, java.math.MathContext.UNLIMITED) + val bd = BigDecimal(bi, java.math.MathContext.UNLIMITED.nn) val isByte = bi >= Byte.MinValue && bi <= Byte.MaxValue val isShort = bi >= Short.MinValue && bi <= Short.MaxValue val isChar = bi >= Char.MinValue && bi <= Char.MaxValue diff --git a/tests/run/t1987.scala b/tests/run/t1987.scala index f5abc262cc9f..592d2afd4bcc 100644 --- a/tests/run/t1987.scala +++ b/tests/run/t1987.scala @@ -56,7 +56,7 @@ package bip { object Test { def main(args: Array[String]): Unit = { - foo.bar.Main.main(null) - bip.bar.Main.main(null) + foo.bar.Main.main(Array.empty[String]) + bip.bar.Main.main(Array.empty[String]) } } From f611bf4ef0bd5d66d9454b77cfc82d3a59938fa2 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 26 Nov 2018 16:04:41 -0500 Subject: [PATCH 071/127] Test that we can call .nn on a non-null type --- tests/pos/explicit-null-nn/S.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/pos/explicit-null-nn/S.scala b/tests/pos/explicit-null-nn/S.scala index 85e6f4581882..819f080eab0c 100644 --- a/tests/pos/explicit-null-nn/S.scala +++ b/tests/pos/explicit-null-nn/S.scala @@ -4,4 +4,12 @@ class S { // nullability from a type. val s: String = j.foo.nn val a: Array[String|Null] = j.bar.nn + + // We can also call .nn on non-nullable types. + val x: String = ??? + val y: String = x.nn + + // And on other Scala code. + val x2: String|Null = null + val y2: String = x2.nn } From eb47a28a39aa823e9f7714cc63f130758ccfd3a0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 29 Nov 2018 12:47:25 -0500 Subject: [PATCH 072/127] Fix lots of tests --- tests/run/erased-frameless.scala | 8 ++++---- tests/run/i3006b.scala | 6 +++--- tests/run/i4404c.scala | 4 ++-- tests/run/nothing-val.scala | 2 +- tests/run/null-and-intersect.scala | 6 +++--- tests/run/null-lazy-val.scala | 2 +- tests/run/null-val.scala | 2 +- tests/run/t1005.scala | 4 ++-- tests/run/t3452a/S_1.scala | 2 +- tests/run/t3452b-bcode/S_1.scala | 4 ++-- tests/run/t6793.scala | 2 +- tests/run/t7120b.scala | 4 ++-- tests/run/t7231.scala | 4 ++-- tests/run/t8087.scala | 2 +- tests/run/unit-volatile-var.scala | 2 +- 15 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/run/erased-frameless.scala b/tests/run/erased-frameless.scala index 5c18ef378d5f..c45ec951d52d 100644 --- a/tests/run/erased-frameless.scala +++ b/tests/run/erased-frameless.scala @@ -71,17 +71,17 @@ trait Exists[T, K, V] object Exists { implicit def derive[T, H <: HList, K, V](implicit g: LabelledGeneric[T] { type Repr = H }, s: Selector[H, K, V]): Exists[T, K, V] = { println("Exists.derive") - null + new Exists[T, K, V] {} } implicit def caseFound[T <: HList, K <: String, V]: Selector[R[K, V] :: T, K, V] = { println("Selector.caseFound") - null + new Selector[R[K, V] :: T, K, V] {} } implicit def caseRecur[H, T <: HList, K <: String, V](implicit i: Selector[T, K, V]): Selector[H :: T, K, V] = { println("Selector.caseRecur") - null + new Selector[H :: T, K, V] {} } } @@ -96,7 +96,7 @@ object X4 { type Repr = R["a", A] :: R["b", B] :: R["c", C] :: R["d", D] :: HNil } = { println("X4.x4Repr") - null + new LabelledGeneric[X4[A, B, C, D]] { type Repr = R["a", A] :: R["b", B] :: R["c", C] :: R["d", D] :: HNil } } } diff --git a/tests/run/i3006b.scala b/tests/run/i3006b.scala index 29aab964268b..503a5da58d04 100644 --- a/tests/run/i3006b.scala +++ b/tests/run/i3006b.scala @@ -1,19 +1,19 @@ class Foo(val x: String) { def this() = this({ - def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn bar() }) def this(i: Int) = this({ - def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn bar() }) } class Bar(val x: String) { def this() = this({ - def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def bar() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn bar() }) } diff --git a/tests/run/i4404c.scala b/tests/run/i4404c.scala index 29d504d57bce..4ffc4ce8128b 100644 --- a/tests/run/i4404c.scala +++ b/tests/run/i4404c.scala @@ -15,9 +15,9 @@ class C { object Test { def main(args: Array[String]) = { List((new T {}).getClass.getInterfaces.head, (new C).getClass) - .map(_.getDeclaredMethods) + .map(_.nn.getDeclaredMethods) .foreach { ms => - println(ms.exists(m => Modifier.isFinal(m.getModifiers))) + println(ms.nn.exists(m => Modifier.isFinal(m.nn.getModifiers))) } } } diff --git a/tests/run/nothing-val.scala b/tests/run/nothing-val.scala index 104be9e435b6..03fe75e6e87f 100644 --- a/tests/run/nothing-val.scala +++ b/tests/run/nothing-val.scala @@ -10,7 +10,7 @@ object Test { case e: NotImplementedError => println("???") } - assert(!classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!classOf[Foo].getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } diff --git a/tests/run/null-and-intersect.scala b/tests/run/null-and-intersect.scala index 7266dabe6df4..8c9afeb7aa56 100644 --- a/tests/run/null-and-intersect.scala +++ b/tests/run/null-and-intersect.scala @@ -3,17 +3,17 @@ object Test { class Bippy extends Immutable with Immortal class Boppy extends Immutable - def f[T](x: Traversable[T]) = x match { + def f[T](x: Traversable[T]|Null) = x match { case _: Map[_, _] => 3 case _: Seq[_] => 2 case _: Iterable[_] => 1 case _ => 4 } - def g(x: Bippy) = x match { + def g(x: Bippy|Null) = x match { case _: Immutable with Immortal => 1 case _ => 2 } - def h(x: Immutable) = x match { + def h(x: Immutable|Null) = x match { case _: Immortal => 1 case _ => 2 } diff --git a/tests/run/null-lazy-val.scala b/tests/run/null-lazy-val.scala index fb7e78f730f0..c61329ee3470 100644 --- a/tests/run/null-lazy-val.scala +++ b/tests/run/null-lazy-val.scala @@ -10,7 +10,7 @@ object Test { // TODO: Erase // Currently not erasing fields for lazy vals - assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + assert(f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") } diff --git a/tests/run/null-val.scala b/tests/run/null-val.scala index aa2637ee1a42..79fb259278d4 100644 --- a/tests/run/null-val.scala +++ b/tests/run/null-val.scala @@ -4,7 +4,7 @@ object Test { val f = new Foo println(f.foo) println(f.foo) - assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } } diff --git a/tests/run/t1005.scala b/tests/run/t1005.scala index 562e2e4c6d50..317c02cc4c37 100644 --- a/tests/run/t1005.scala +++ b/tests/run/t1005.scala @@ -5,11 +5,11 @@ object Test class Bar[T](x : Array[T]) { def bar = x.asInstanceOf[Array[AnyRef]] } object FromMono{ - def mainer(args : Array[String]) = (new Foo[AnyRef](Array[AnyRef]("Halp!"))).bar + def mainer(args : Array[String]|Null) = (new Foo[AnyRef](Array[AnyRef]("Halp!"))).bar } object FromPoly{ - def mainer(args : Array[String]) = (new Bar[AnyRef](Array[AnyRef]("Halp!"))).bar + def mainer(args : Array[String]|Null) = (new Bar[AnyRef](Array[AnyRef]("Halp!"))).bar } def main(args: Array[String]): Unit = { diff --git a/tests/run/t3452a/S_1.scala b/tests/run/t3452a/S_1.scala index 791faf42fa40..38e90de9048b 100644 --- a/tests/run/t3452a/S_1.scala +++ b/tests/run/t3452a/S_1.scala @@ -3,7 +3,7 @@ abstract class BulkSearch { type Rel <: Relation [R] type Corr <: Correspondence[R] - def searchFor(input: Rel): Mapping[Corr] = { println("BulkSearch.searchFor called.") ; null } + def searchFor(input: Rel): Mapping[Corr] = { println("BulkSearch.searchFor called.") ; new Mapping[Corr] } } object BulkSearchInstance extends BulkSearch { diff --git a/tests/run/t3452b-bcode/S_1.scala b/tests/run/t3452b-bcode/S_1.scala index a209f1203539..0e8036ada023 100644 --- a/tests/run/t3452b-bcode/S_1.scala +++ b/tests/run/t3452b-bcode/S_1.scala @@ -1,14 +1,14 @@ trait Search[M] { def search(input: M): C[Int] = { println("Search received: " + input) - null + new C[Int] {} } } class SearchC[M] { def searchC(input: M): C[Int] = { println("SearchC received: " + input) - null + new C[Int] {} } } diff --git a/tests/run/t6793.scala b/tests/run/t6793.scala index 3d8492c10469..78b15140bf8c 100644 --- a/tests/run/t6793.scala +++ b/tests/run/t6793.scala @@ -5,5 +5,5 @@ object Test extends dotty.runtime.LegacyApp { new b.C2("x") val c2Fields = classOf[b.C2].getDeclaredFields - assert(c2Fields.size == 1, c2Fields.map(_.getName).toList) + assert(c2Fields.size == 1, c2Fields.map(_.nn.getName).toList) } diff --git a/tests/run/t7120b.scala b/tests/run/t7120b.scala index e5b546a90e07..aab76d672f50 100644 --- a/tests/run/t7120b.scala +++ b/tests/run/t7120b.scala @@ -20,11 +20,11 @@ class CHK extends BaseHK[BaseHK.Id, String] { object Test extends dotty.runtime.LegacyApp { val c = new C val d = new c.D() - val meth = d.getClass.getMethods.find(_.getName == "foo").get + val meth = d.getClass.getMethods.find(_.nn.getName == "foo").get println(meth) val chk = new CHK val dhk = new chk.D() - val methhk = d.getClass.getMethods.find(_.getName == "foo").get + val methhk = d.getClass.getMethods.find(_.nn.getName == "foo").get println(methhk) } diff --git a/tests/run/t7231.scala b/tests/run/t7231.scala index 63ed16097348..9ae33909defe 100644 --- a/tests/run/t7231.scala +++ b/tests/run/t7231.scala @@ -1,8 +1,8 @@ object Test extends dotty.runtime.LegacyApp { val bar: Null = null - def foo(x: Array[Int]) = x - def baz(x: String) = x + def foo(x: Array[Int]|Null) = x + def baz(x: String|Null) = x // first line was failing println(foo(bar)) diff --git a/tests/run/t8087.scala b/tests/run/t8087.scala index 7306039c6509..0532efe9566b 100644 --- a/tests/run/t8087.scala +++ b/tests/run/t8087.scala @@ -7,6 +7,6 @@ class Bar extends Foo object Test extends dotty.runtime.LegacyApp { classOf[Bar].getDeclaredFields.foreach(f => { - assert(java.lang.reflect.Modifier.isVolatile(f.getModifiers), f.getName) + assert(java.lang.reflect.Modifier.isVolatile(f.nn.getModifiers), f.nn.getName) }) } diff --git a/tests/run/unit-volatile-var.scala b/tests/run/unit-volatile-var.scala index 1c7a77fd6d40..7e875fe2d1ed 100644 --- a/tests/run/unit-volatile-var.scala +++ b/tests/run/unit-volatile-var.scala @@ -8,7 +8,7 @@ object Test { println("foo3") } f.foo - assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "@volatile field foo erased") + assert(f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "@volatile field foo erased") } } From 47196c49b6549ee4481a3390771e5e98005a72e0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 29 Nov 2018 13:26:11 -0500 Subject: [PATCH 073/127] A couple more tests --- tests/run/t1335.scala | 2 +- tests/run/try.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run/t1335.scala b/tests/run/t1335.scala index 047f7b566a70..89f72270dc5d 100644 --- a/tests/run/t1335.scala +++ b/tests/run/t1335.scala @@ -3,7 +3,7 @@ case class MyTuple(a: Int, b: Int) object Test { def main(args: Array[String]): Unit = try { - val mt: MyTuple = null + val mt: MyTuple|Null = null val MyTuple(a, b) = mt } catch { case e: MatchError => () diff --git a/tests/run/try.scala b/tests/run/try.scala index 785f0153a7a5..2875ce9cf6c5 100644 --- a/tests/run/try.scala +++ b/tests/run/try.scala @@ -91,7 +91,7 @@ object Test extends AnyRef with App { def get = null } - var sekw : SekwencjaArray = + var sekw : SekwencjaArray|Null = try { null } catch { @@ -99,7 +99,7 @@ object Test extends AnyRef with App { } new AnyRef { - def getValueAt(row:Int, col:Int) = sekw.get + def getValueAt(row:Int, col:Int) = sekw.nn.get } } From d0adc486383c874b466ff1e7d7bab18f07f981ea Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 5 Dec 2018 13:26:43 -0500 Subject: [PATCH 074/127] [WIP] Flow-sensitive type inference for nullability --- .../src/dotty/tools/dotc/core/Contexts.scala | 10 +++- .../src/dotty/tools/dotc/core/FlowFacts.scala | 54 +++++++++++++++++++ .../src/dotty/tools/dotc/core/Types.scala | 49 +++++++++++++++-- .../src/dotty/tools/dotc/typer/Typer.scala | 26 ++++++--- 4 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/core/FlowFacts.scala diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index b212e3cbfcfb..5e5df2569328 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -22,13 +22,14 @@ import reporting._ import reporting.diagnostic.Message import io.AbstractFile import scala.io.Codec + import collection.mutable import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings} import scala.annotation.internal.sharable - import DenotTransformers.DenotTransformer +import dotty.tools.dotc.core.FlowFacts.NonNullSet import dotty.tools.dotc.profile.Profiler import util.Property.Key import util.Store @@ -142,6 +143,11 @@ object Contexts { protected def gadt_=(gadt: GADTMap): Unit = _gadt = gadt def gadt: GADTMap = _gadt + /** The terms currently known to be non-null (in spite of their declared type) */ + private[this] var _nonNullFacts: NonNullSet = _ + protected def nonNullFacts_=(nnSet: NonNullSet): Unit = _nonNullFacts = nnSet + def nonNullFacts: NonNullSet = _nonNullFacts + /** The history of implicit searches that are currently active */ private[this] var _searchHistory: SearchHistory = null protected def searchHistory_= (searchHistory: SearchHistory): Unit = _searchHistory = searchHistory @@ -487,6 +493,7 @@ object Contexts { def setImportInfo(importInfo: ImportInfo): this.type = { this.importInfo = importInfo; this } def setGadt(gadt: GADTMap): this.type = { this.gadt = gadt; this } def setFreshGADTBounds: this.type = setGadt(gadt.fresh) + def setNonNullFacts(nnSet: NonNullSet): this.type = { this.nonNullFacts = nnSet; this } def setSearchHistory(searchHistory: SearchHistory): this.type = { this.searchHistory = searchHistory; this } def setTypeComparerFn(tcfn: Context => TypeComparer): this.type = { this.typeComparer = tcfn(this); this } private def setMoreProperties(moreProperties: Map[Key[Any], Any]): this.type = { this.moreProperties = moreProperties; this } @@ -563,6 +570,7 @@ object Contexts { typeComparer = new TypeComparer(this) searchHistory = new SearchRoot gadt = EmptyGADTMap + nonNullFacts = FlowFacts.emptyNonNullSet } @sharable object NoContext extends Context { diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala new file mode 100644 index 000000000000..7cd8b680d569 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -0,0 +1,54 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.ast.Trees.{Apply, Literal, Select} +import dotty.tools.dotc.ast.tpd._ +import StdNames.nme +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Types.{NonNullTermRef, TermRef, Type} + +import scala.annotation.internal.sharable + +/** Operations on flow-sensitive type information + * + * Currently the following idioms are supported: + * (1) + * ``` + * val x: String|Null = "foo" + * if (x != null) { + * // x: String in the "then" branch + * } + * ``` + * Notice that `x` must be stable for the above to work. + */ +object FlowFacts { + + /** A set of `TermRef`s known to be non-nullable at the current program point */ + type NonNullSet = Set[TermRef] + + /** The initial state where no `TermRef`s are known to be non-null */ + @sharable val emptyNonNullSet = Set.empty[TermRef] + + /** Is `tref` non-null (even if its info says it isn't)? */ + def isNonNull(nnSet: NonNullSet, tref: TermRef): Boolean = { + nnSet.contains(tref) + } + + /** Tries to improve de "precision" of `tpe` using flow-sensitive type information. */ + def refineType(tpe: Type)(implicit ctx: Context): Type = tpe match { + case tref: TermRef if isNonNull(ctx.nonNullFacts, tref) => + NonNullTermRef.fromTermRef(tref) + case _ => tpe + } + + /** Analyze the control statement in `tree` to learn new facts about non-nullability. */ + def inferNonNull(tree: Tree)(implicit ctx: Context): NonNullSet = { + tree match { + case Apply(Select(lhs, nme.NE), List(Literal(const))) if const.tag == Constants.NullTag => + lhs.tpe match { + case tref: TermRef if tref.isStable => Set(tref) + case _ => emptyNonNullSet + } + case _ => emptyNonNullSet + } + } +} diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 52957376e55a..687b98367d1e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1779,6 +1779,13 @@ object Types { private[this] var checkedPeriod: Period = Nowhere private[this] var myStableHash: Byte = 0 + // TODO(abeln): remove this hack. + // This is a hack to detect whether `this` is an instance of `NonNullTermRef`, and adjust + // the denotation accordingly. The "adjustment" happens in `computeDenot`, which is currently + // marked as private. + // Figure out a different code structure. + protected val isNonNull: Boolean = false + // Invariants: // (1) checkedPeriod != Nowhere => lastDenotation != null // (2) lastDenotation != null => lastSymbol != null @@ -1884,14 +1891,21 @@ object Types { private def computeDenot(implicit ctx: Context): Denotation = { def finish(d: Denotation) = { - if (d.exists) + if (d.exists) { // Avoid storing NoDenotations in the cache - we will not be able to recover from // them. The situation might arise that a type has NoDenotation in some later // phase but a defined denotation earlier (e.g. a TypeRef to an abstract type // is undefined after erasure.) We need to be able to do time travel back and // forth also in these cases. - setDenot(d) - d + + // TODO(abeln): remove once `isNonNull` is gone. + // If the denotation is computed for the first time, or if it's ever updated, make sure + // that the `info` is non-null. + val d1 = if (isNonNull) d.mapInfo(_.stripNull) else d + + setDenot(d1) + d1 + } else d } def fromDesignator = designator match { @@ -2291,7 +2305,7 @@ object Types { private var myDesignator: Designator) extends NamedType with SingletonType with ImplicitRef { - type ThisType = TermRef + type ThisType >: this.type <: TermRef type ThisName = TermName override def designator: Designator = myDesignator @@ -2342,10 +2356,37 @@ object Types { } final class CachedTermRef(prefix: Type, designator: Designator, hc: Int) extends TermRef(prefix, designator) { + type ThisType = CachedTermRef + assert((prefix ne NoPrefix) || designator.isInstanceOf[Symbol]) myHash = hc } + /** A `TermRef` that, through flow-sensitive type inference, we know is non-null. + * Accordingly, the `info` in its denotation won't be of the form `T|Null`. + * Notice that this class isn't cached, unlike the regular `TermRef`. + */ + final class NonNullTermRef(prefix: Type, designator: Designator) extends TermRef(prefix, designator) { + type ThisType = NonNullTermRef + + // This allows `NamedType` to identify `NonNullTermRef` from all other named types. + override protected val isNonNull: Boolean = true + } + + object NonNullTermRef { + + /** Create a `TermRef` that's just like `tref`, but whose `info` is always non-null. */ + def fromTermRef(tref: TermRef)(implicit ctx: Context): NonNullTermRef = { + val denot = tref.denot.mapInfo(_.stripNull) + val nn = new NonNullTermRef(tref.prefix, denot.symbol) + // We need to set the non-null denotation directly because normally the "non-nullable" denotations + // are created in `computeDenot`, but they _won't_ be computed if the original `tref` _already_ had + // a cached denotation. + // This is brittle, since `This` + nn.withDenot(denot) + } + } + final class CachedTypeRef(prefix: Type, designator: Designator, hc: Int) extends TypeRef(prefix, designator) { assert((prefix ne NoPrefix) || designator.isInstanceOf[Symbol]) myHash = hc diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 07a55a9288cf..e53f6a02c6a8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -396,11 +396,13 @@ class Typer extends Namer } else errorType(new MissingIdent(tree, kind, name.show), tree.pos) - val tree1 = ownType match { - case ownType: NamedType if !prefixIsElidable(ownType) => - ref(ownType).withPos(tree.pos) + val ownType1 = FlowFacts.refineType(ownType) + + val tree1 = ownType1 match { + case ownType1: NamedType if !prefixIsElidable(ownType1) => + ref(ownType1).withPos(tree.pos) case _ => - tree.withType(ownType) + tree.withType(ownType1) } checkStableIdentPattern(tree1, pt) @@ -420,8 +422,11 @@ class Typer extends Namer tree } - private def typedSelect(tree: untpd.Select, pt: Type, qual: Tree)(implicit ctx: Context): Select = - checkValue(assignType(cpy.Select(tree)(qual, tree.name), qual), pt) + private def typedSelect(tree: untpd.Select, pt: Type, qual: Tree)(implicit ctx: Context): Select = { + val select = assignType(cpy.Select(tree)(qual, tree.name), qual) + val select1 = select.withType(FlowFacts.refineType(select.tpe)) + checkValue(select1, pt) + } def typedSelect(tree: untpd.Select, pt: Type)(implicit ctx: Context): Tree = track("typedSelect") { @@ -727,8 +732,15 @@ class Typer extends Namer def typedIf(tree: untpd.If, pt: Type)(implicit ctx: Context): Tree = track("typedIf") { if (tree.isInline) checkInInlineContext("inline if", tree.pos) val cond1 = typed(tree.cond, defn.BooleanType) + val newFacts = FlowFacts.inferNonNull(cond1) + // TODO(abeln): generalize + val thenCtx = if (newFacts.isEmpty) { + ctx + } else { + ctx.fresh.setNonNullFacts(ctx.nonNullFacts ++ newFacts) + } val thenp2 :: elsep2 :: Nil = harmonic(harmonize, pt) { - val thenp1 = typed(tree.thenp, pt.notApplied) + val thenp1 = typed(tree.thenp, pt.notApplied)(thenCtx) val elsep1 = typed(tree.elsep orElse (untpd.unitLiteral withPos tree.pos), pt.notApplied) thenp1 :: elsep1 :: Nil } From d02713532e32e30c6742439053baa5a100cbed3a Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 5 Dec 2018 16:20:36 -0500 Subject: [PATCH 075/127] Add basic test for flow inference Until we get support for flow inference in Tasty, we'll have to add any positive tests that use flow sensitivity to the pickling blacklist. --- .../test/dotc/pos-test-pickling.blacklist | 1 + tests/neg/explicit-null-flow.scala | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/neg/explicit-null-flow.scala diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 90cd8dbf757e..cdc07704b8c5 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -10,3 +10,4 @@ t3249 t3486 t3612.scala typelevel0.scala +Transactions.scala diff --git a/tests/neg/explicit-null-flow.scala b/tests/neg/explicit-null-flow.scala new file mode 100644 index 000000000000..7e565ef9e92d --- /dev/null +++ b/tests/neg/explicit-null-flow.scala @@ -0,0 +1,43 @@ + +// Flow-sensitive type inference +class Foo { + + class Bar { + val s: String = ??? + } + + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + val s = b.s // error: `b` is `Bar|Null` + } + val s = b.s // error: `b` is `Bar|Null` + + + var b2: Bar|Null = ??? + if (b2 != null) { + val s = b.s // error: type of `b2` isn't refined because `b2` is not stable + } + + class Bar2 { + val x: Bar2|Null = ??? + } + + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x + val b2err: Bar2 = bar2.x.x.x // error: expected Bar2 but got Bar2|Null + } + val b2: Bar2 = bar2.x + } + val b2: Bar2 = bar2 + } + +} From 7d1c3bab10635a16940f2d85cb0d92d40ed1f6da Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 6 Dec 2018 17:26:17 -0500 Subject: [PATCH 076/127] Better flow-sensitive type inference This adds support for conditions with &&s, ||s and unary negation. Still TODO is propagating the flow information _within_ the cond as it's typed. --- .../src/dotty/tools/dotc/core/FlowFacts.scala | 117 ++++++++++++++---- .../src/dotty/tools/dotc/typer/Typer.scala | 14 +-- .../test/dotc/pos-test-pickling.blacklist | 1 + tests/neg/explicit-null-flow.scala | 90 ++++++++++++++ tests/pos/explicit-null-flow.scala | 115 +++++++++++++++++ 5 files changed, 308 insertions(+), 29 deletions(-) create mode 100644 tests/pos/explicit-null-flow.scala diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 7cd8b680d569..3edba6f335da 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -4,22 +4,12 @@ import dotty.tools.dotc.ast.Trees.{Apply, Literal, Select} import dotty.tools.dotc.ast.tpd._ import StdNames.nme import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.Types.{NonNullTermRef, TermRef, Type} import scala.annotation.internal.sharable -/** Operations on flow-sensitive type information - * - * Currently the following idioms are supported: - * (1) - * ``` - * val x: String|Null = "foo" - * if (x != null) { - * // x: String in the "then" branch - * } - * ``` - * Notice that `x` must be stable for the above to work. - */ +/** Operations on flow-sensitive type information */ object FlowFacts { /** A set of `TermRef`s known to be non-nullable at the current program point */ @@ -28,6 +18,7 @@ object FlowFacts { /** The initial state where no `TermRef`s are known to be non-null */ @sharable val emptyNonNullSet = Set.empty[TermRef] + /** Is `tref` non-null (even if its info says it isn't)? */ def isNonNull(nnSet: NonNullSet, tref: TermRef): Boolean = { nnSet.contains(tref) @@ -40,15 +31,99 @@ object FlowFacts { case _ => tpe } - /** Analyze the control statement in `tree` to learn new facts about non-nullability. */ - def inferNonNull(tree: Tree)(implicit ctx: Context): NonNullSet = { - tree match { - case Apply(Select(lhs, nme.NE), List(Literal(const))) if const.tag == Constants.NullTag => - lhs.tpe match { - case tref: TermRef if tref.isStable => Set(tref) - case _ => emptyNonNullSet - } - case _ => emptyNonNullSet + /** Nullability facts inferred from a condition. + * `ifTrue` are the terms known to be non-null if the condition is true. + * `ifFalse` are the terms known to be non-null if the condition is false. + */ + case class Inferred(ifTrue: NonNullSet, ifFalse: NonNullSet) { + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculates the inferred facts for + * (short-circuited) `e1 && e2` using De Morgan's laws. + */ + def combineAnd(other: Inferred): Inferred = Inferred(ifTrue.union(other.ifTrue), ifFalse.intersect(other.ifFalse)) + + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculates the inferred facts for + * (short-circuited) `e1 || e2` using De Morgan's laws. + */ + def combineOr(other: Inferred): Inferred = Inferred(ifTrue.intersect(other.ifTrue), ifFalse.union(other.ifFalse)) + + /** The inferred facts for the negation of this condition. */ + def negate: Inferred = Inferred(ifFalse, ifTrue) + + val isEmpty: Boolean = ifTrue.isEmpty && ifFalse.isEmpty + } + + object Inferred { + /** Create a singleton inferred fact containing `tref`. */ + def apply(tref: TermRef, ifTrue: Boolean): Inferred = { + if (ifTrue) Inferred(Set(tref), emptyNonNullSet) + else Inferred(emptyNonNullSet, Set(tref)) + } + } + + /** Analyze the tree for a condition `cond` to learn new facts about non-nullability. + * Supports ands, ors, and unary negation. + * + * Example: + * (1) + * ``` + * val x: String|Null = "foo" + * if (x != null) { + * // x: String in the "then" branch + * } + * ``` + * Notice that `x` must be stable for the above to work. + * + * TODO(abeln): add longer description of the algorithm + */ + def inferNonNull(cond: Tree)(implicit ctx: Context): Inferred = { + val emptyFacts = Inferred(emptyNonNullSet, emptyNonNullSet) + + /** Combine two sets of facts according to `op`. */ + def combine(lhs: Inferred, op: Name, rhs: Inferred): Inferred = { + if (op == nme.ZAND) lhs.combineAnd(rhs) + else lhs.combineOr(rhs) } + + /** Recurse over a conditional to extract flow facts. */ + def recur(tree: Tree): Inferred = { + tree match { + case Apply(Select(lhs, op), List(rhs)) => + if (op == nme.ZAND || op == nme.ZOR) combine(recur(lhs), op, recur(rhs)) + else if (op == nme.EQ || op == nme.NE) newFact(lhs, isEq = op == nme.EQ, rhs) + else emptyFacts + case Select(lhs, neg) if neg == nme.UNARY_! => + recur(lhs).negate + case _ => emptyFacts + } + } + + /** Extract new facts from an expression `lhs = rhs` or `lhs != rhs` + * if either the lhs or rhs is the `null` literal. + */ + def newFact(lhs: Tree, isEq: Boolean, rhs: Tree): Inferred = { + def isNullLit(tree: Tree): Boolean = tree match { + case Literal(const) if const.tag == Constants.NullTag => true + case _ => false + } + + def isStableTermRef(tree: Tree): Boolean = asStableTermRef(tree).isDefined + + def asStableTermRef(tree: Tree): Option[TermRef] = tree.tpe match { + case tref: TermRef if tref.isStable => Some(tref) + case _ => None + } + + val trefOpt = + if (isNullLit(lhs) && isStableTermRef(rhs)) asStableTermRef(rhs) + else if (isStableTermRef(lhs) && isNullLit(rhs)) asStableTermRef(lhs) + else None + + trefOpt match { + case Some(tref) => Inferred(tref, ifTrue = !isEq) + case _ => emptyFacts + } + } + + recur(cond) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e53f6a02c6a8..82bab602e4ab 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -36,6 +36,7 @@ import util.Stats.{record, track} import config.Printers.{gadts, typr} import rewrites.Rewrites.patch import NavigateAST._ +import dotty.tools.dotc.core.FlowFacts.{Inferred, NonNullSet} import transform.SymUtils._ import transform.TypeUtils._ import reporting.trace @@ -731,17 +732,14 @@ class Typer extends Namer def typedIf(tree: untpd.If, pt: Type)(implicit ctx: Context): Tree = track("typedIf") { if (tree.isInline) checkInInlineContext("inline if", tree.pos) + def withFacts(facts: NonNullSet): Context = if (facts.isEmpty) ctx else ctx.fresh.setNonNullFacts(ctx.nonNullFacts ++ facts) val cond1 = typed(tree.cond, defn.BooleanType) - val newFacts = FlowFacts.inferNonNull(cond1) - // TODO(abeln): generalize - val thenCtx = if (newFacts.isEmpty) { - ctx - } else { - ctx.fresh.setNonNullFacts(ctx.nonNullFacts ++ newFacts) - } + val Inferred(ifTrue, ifFalse) = FlowFacts.inferNonNull(cond1) + val thenCtx = withFacts(ifTrue) + val elseCtx = withFacts(ifFalse) val thenp2 :: elsep2 :: Nil = harmonic(harmonize, pt) { val thenp1 = typed(tree.thenp, pt.notApplied)(thenCtx) - val elsep1 = typed(tree.elsep orElse (untpd.unitLiteral withPos tree.pos), pt.notApplied) + val elsep1 = typed(tree.elsep orElse (untpd.unitLiteral withPos tree.pos), pt.notApplied)(elseCtx) thenp1 :: elsep1 :: Nil } assignType(cpy.If(tree)(cond1, thenp2, elsep2), thenp2, elsep2) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index cdc07704b8c5..ec95f5bb6f5f 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -11,3 +11,4 @@ t3486 t3612.scala typelevel0.scala Transactions.scala +explicit-null-flow.scala diff --git a/tests/neg/explicit-null-flow.scala b/tests/neg/explicit-null-flow.scala index 7e565ef9e92d..05ce19f257b4 100644 --- a/tests/neg/explicit-null-flow.scala +++ b/tests/neg/explicit-null-flow.scala @@ -6,6 +6,7 @@ class Foo { val s: String = ??? } + // Basic val b: Bar|Null = ??? if (b != null) { val s = b.s // ok: type of `b` inferred as `Bar` @@ -16,6 +17,7 @@ class Foo { val s = b.s // error: `b` is `Bar|Null` + // Not stable var b2: Bar|Null = ??? if (b2 != null) { val s = b.s // error: type of `b2` isn't refined because `b2` is not stable @@ -25,6 +27,7 @@ class Foo { val x: Bar2|Null = ??? } + // Nested and selection val bar2: Bar2|Null = ??? if (bar2 != null) { if (bar2.x != null) { @@ -40,4 +43,91 @@ class Foo { val b2: Bar2 = bar2 } + // If-then-else and equality + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } + + // Elseif + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + val err1 = s2.length // error + val err2 = s3.length // error + } else if (s2 != null) { + val len = s2.length + val err1 = s1.length // error + val err2 = s3.length // error + } else if (s3 != null) { + val len = s3.length + val err1 = s1.length // error + val err2 = s2.length // error + } + + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } + + // Common idioms + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + // Basic negation + if (!(s1 != null)) { + val len = s1.length // error + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } + + // Parens + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } + + // Operator precedence + if (s1 != null || s2 != null && s3 != null) { + val len = s3.length // error + } + + if (s1 != null && s2 != null || s3 != null) { + val len1 = s1.length // error + val len2 = s2.length // error + val len3 = s3.length // error + } + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + val len2 = s2.length // error + val len3 = s3.length // error + } } diff --git a/tests/pos/explicit-null-flow.scala b/tests/pos/explicit-null-flow.scala new file mode 100644 index 000000000000..4e0fc59e4971 --- /dev/null +++ b/tests/pos/explicit-null-flow.scala @@ -0,0 +1,115 @@ + +// Flow-sensitive type inference +class Foo { + + class Bar { + val s: String = ??? + } + + // Basic + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + } + + // Not stable + var b2: Bar|Null = ??? + if (b2 != null) { + } + + class Bar2 { + val x: Bar2|Null = ??? + } + + // Nested and selection + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x + } + val b2: Bar2 = bar2.x + } + val b2: Bar2 = bar2 + } + + // If-then-else and equality + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } + + // Elseif + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + } else if (s2 != null) { + val len = s2.length + } else if (s3 != null) { + val len = s3.length + } + + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } + + // Common idioms + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + // Basic negation + if (!(s1 != null)) { + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } + + // Parens + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } + + // Operator precedence + if (s1 != null || s2 != null && s3 != null) { + } + + if (s1 != null && s2 != null || s3 != null) { + } + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + } +} From 1aa0a1b6a649057ce0120c5841d9d49b04a4f851 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 7 Dec 2018 14:38:06 -0500 Subject: [PATCH 077/127] Add flow inference inside boolean conditions We can now support ``` val s: String|Null = ??? if (s != null && s.length > 0) ``` and ``` if (s == null || s.length == 0) ``` --- .../src/dotty/tools/dotc/core/Contexts.scala | 3 +- .../src/dotty/tools/dotc/core/FlowFacts.scala | 34 ++- .../dotty/tools/dotc/typer/Applications.scala | 36 ++- .../src/dotty/tools/dotc/typer/Typer.scala | 5 +- .../test/dotc/pos-test-pickling.blacklist | 1 + tests/neg/explicit-null-flow.scala | 247 +++++++++++------- tests/pos/explicit-null-flow.scala | 207 +++++++++------ tests/pos/t1107a.scala | 1 - 8 files changed, 330 insertions(+), 204 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 5e5df2569328..ab426f1bd886 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -493,7 +493,8 @@ object Contexts { def setImportInfo(importInfo: ImportInfo): this.type = { this.importInfo = importInfo; this } def setGadt(gadt: GADTMap): this.type = { this.gadt = gadt; this } def setFreshGADTBounds: this.type = setGadt(gadt.fresh) - def setNonNullFacts(nnSet: NonNullSet): this.type = { this.nonNullFacts = nnSet; this } + def setNonNullFacts(facts: NonNullSet): this.type = { this.nonNullFacts = facts; this } + def addNonNullFacts(facts: NonNullSet): this.type = { setNonNullFacts(this.nonNullFacts ++ facts); this } def setSearchHistory(searchHistory: SearchHistory): this.type = { this.searchHistory = searchHistory; this } def setTypeComparerFn(tcfn: Context => TypeComparer): this.type = { this.typeComparer = tcfn(this); this } private def setMoreProperties(moreProperties: Map[Key[Any], Any]): this.type = { this.moreProperties = moreProperties; this } diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 3edba6f335da..7ac029a0e8a2 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -48,8 +48,6 @@ object FlowFacts { /** The inferred facts for the negation of this condition. */ def negate: Inferred = Inferred(ifFalse, ifTrue) - - val isEmpty: Boolean = ifTrue.isEmpty && ifFalse.isEmpty } object Inferred { @@ -80,8 +78,10 @@ object FlowFacts { /** Combine two sets of facts according to `op`. */ def combine(lhs: Inferred, op: Name, rhs: Inferred): Inferred = { - if (op == nme.ZAND) lhs.combineAnd(rhs) - else lhs.combineOr(rhs) + op match { + case _ if op == nme.ZAND => lhs.combineAnd(rhs) + case _ if op == nme.ZOR => lhs.combineOr(rhs) + } } /** Recurse over a conditional to extract flow facts. */ @@ -119,11 +119,35 @@ object FlowFacts { else None trefOpt match { - case Some(tref) => Inferred(tref, ifTrue = !isEq) + case Some(tref) => + // If `isEq`, then the condition is of the form e.g. `lhs == null`, + // in which case we know `lhs` is non-null if the condition is false. + Inferred(tref, ifTrue = !isEq) case _ => emptyFacts } } recur(cond) } + + /** Propagate flow-sensitive type information inside a condition. + * Specifically, if `cond` is of the form `lhs &&` or `lhs ||`, where the lhs has already been typed + * (and the rhs hasn't been typed yet), compute the non-nullability info we get from lhs and + * return a new context with it. The new context can then be used to type the rhs. + * + * This is useful in e.g. + * ``` + * val x: String|Null = ??? + * if (x != null && x.length > 0) ... + * ``` + */ + def propagateWithinCond(cond: Tree)(implicit ctx: Context): Context = { + cond match { + case Select(lhs, op) if op == nme.ZAND || op == nme.ZOR => + val Inferred(ifTrue, ifFalse) = FlowFacts.inferNonNull(lhs) + if (op == nme.ZAND) ctx.fresh.addNonNullFacts(ifTrue) + else ctx.fresh.addNonNullFacts(ifFalse) + case _ => ctx + } + } } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index b5bb5610b387..389cd07d6d22 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -23,12 +23,12 @@ import Inferencing._ import collection.mutable import config.Printers.{overload, typr, unapp} import TypeApplications._ - import reporting.diagnostic.Message import reporting.trace import Constants.{Constant, IntTag, LongTag} import dotty.tools.dotc.reporting.diagnostic.messages.{NotAnExtractor, UnapplyInvalidNumberOfArguments} import Denotations.SingleDenotation +import dotty.tools.dotc.core.FlowFacts.Inferred object Applications { import tpd._ @@ -784,19 +784,31 @@ trait Applications extends Compatibility { self: Typer with Dynamic => typr.println(i"result failure for $tree with type ${fun1.tpe.widen}, expected = $pt") /** Type application where arguments come from prototype, and no implicits are inserted */ - def simpleApply(fun1: Tree, proto: FunProto)(implicit ctx: Context): Tree = - methPart(fun1).tpe match { - case funRef: TermRef => - val app = - if (proto.allArgTypesAreCurrent()) - new ApplyToTyped(tree, fun1, funRef, proto.typedArgs, pt) - else - new ApplyToUntyped(tree, fun1, funRef, proto, pt)(argCtx(tree)) - convertNewGenericArray(app.result) - case _ => - handleUnexpectedFunType(tree, fun1) + def simpleApply(fun1: Tree, proto: FunProto)(implicit ctx: Context): Tree = { + // TODO(abeln): we're re-doing work here by recomputing what's implies by the lhs of the comparison. + // e.g. in `A && B && C && D`, we'll recompute the facts implied by `A && B` twice. + // Find a more-efficient way to do this. + val ctx1 = FlowFacts.propagateWithinCond(fun1) + + // Separate into a function so we can pass the updated context. + def proc(implicit ctx: Context) = { + proto.withContext(ctx).asInstanceOf[FunProto] + methPart(fun1).tpe match { + case funRef: TermRef => + val app = + if (proto.allArgTypesAreCurrent()) + new ApplyToTyped(tree, fun1, funRef, proto.typedArgs, pt) + else + new ApplyToUntyped(tree, fun1, funRef, proto, pt)(argCtx(tree)) + convertNewGenericArray(app.result) + case _ => + handleUnexpectedFunType(tree, fun1) + } } + proc(ctx1) + } + /** Try same application with an implicit inserted around the qualifier of the function * part. Return an optional value to indicate success. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 82bab602e4ab..fd228f0636c7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -732,11 +732,10 @@ class Typer extends Namer def typedIf(tree: untpd.If, pt: Type)(implicit ctx: Context): Tree = track("typedIf") { if (tree.isInline) checkInInlineContext("inline if", tree.pos) - def withFacts(facts: NonNullSet): Context = if (facts.isEmpty) ctx else ctx.fresh.setNonNullFacts(ctx.nonNullFacts ++ facts) val cond1 = typed(tree.cond, defn.BooleanType) val Inferred(ifTrue, ifFalse) = FlowFacts.inferNonNull(cond1) - val thenCtx = withFacts(ifTrue) - val elseCtx = withFacts(ifFalse) + val thenCtx = ctx.fresh.addNonNullFacts(ifTrue) + val elseCtx = ctx.fresh.addNonNullFacts(ifFalse) val thenp2 :: elsep2 :: Nil = harmonic(harmonize, pt) { val thenp1 = typed(tree.thenp, pt.notApplied)(thenCtx) val elsep1 = typed(tree.elsep orElse (untpd.unitLiteral withPos tree.pos), pt.notApplied)(elseCtx) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index ec95f5bb6f5f..293e2b5c1b0c 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -12,3 +12,4 @@ t3612.scala typelevel0.scala Transactions.scala explicit-null-flow.scala +t1107a.scala diff --git a/tests/neg/explicit-null-flow.scala b/tests/neg/explicit-null-flow.scala index 05ce19f257b4..013b56d3f1c2 100644 --- a/tests/neg/explicit-null-flow.scala +++ b/tests/neg/explicit-null-flow.scala @@ -2,132 +2,181 @@ // Flow-sensitive type inference class Foo { - class Bar { - val s: String = ??? - } + def basic() = { + class Bar { + val s: String = ??? + } - // Basic - val b: Bar|Null = ??? - if (b != null) { - val s = b.s // ok: type of `b` inferred as `Bar` - val s2: Bar = b - } else { - val s = b.s // error: `b` is `Bar|Null` + // Basic + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + val s = b.s // error: `b` is `Bar|Null` + } + val s = b.s // error: `b` is `Bar|Null` } - val s = b.s // error: `b` is `Bar|Null` + def notStable() = { + class Bar { + var s: String = ??? + } - // Not stable - var b2: Bar|Null = ??? - if (b2 != null) { - val s = b.s // error: type of `b2` isn't refined because `b2` is not stable + var b2: Bar|Null = ??? + if (b2 != null) { + val s = b.s // error: type of `b2` isn't refined because `b2` is not stable + } } - class Bar2 { - val x: Bar2|Null = ??? - } + def nested() = { + class Bar2 { + val x: Bar2|Null = ??? + } - // Nested and selection - val bar2: Bar2|Null = ??? - if (bar2 != null) { - if (bar2.x != null) { - if (bar2.x.x != null) { - if (bar2.x.x.x != null) { - val b2: Bar2 = bar2.x.x.x + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x + val b2err: Bar2 = bar2.x.x.x // error: expected Bar2 but got Bar2|Null } - val b2: Bar2 = bar2.x.x - val b2err: Bar2 = bar2.x.x.x // error: expected Bar2 but got Bar2|Null + val b2: Bar2 = bar2.x } - val b2: Bar2 = bar2.x + val b2: Bar2 = bar2 } - val b2: Bar2 = bar2 } - // If-then-else and equality - val s: String|Null = ??? - if (s == null) { - } else { - val len: Int = s.length - val len2 = s.length + def ifThenElse() = { + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } } - // Elseif - val s1: String|Null = ??? - val s2: String|Null = ??? - val s3: String|Null = ??? - if (s1 != null) { - val len = s1.length - val err1 = s2.length // error - val err2 = s3.length // error - } else if (s2 != null) { - val len = s2.length - val err1 = s1.length // error - val err2 = s3.length // error - } else if (s3 != null) { - val len = s3.length - val err1 = s1.length // error - val err2 = s2.length // error + def elseIf() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + val err1 = s2.length // error + val err2 = s3.length // error + } else if (s2 != null) { + val len = s2.length + val err1 = s1.length // error + val err2 = s3.length // error + } else if (s3 != null) { + val len = s3.length + val err1 = s1.length // error + val err2 = s2.length // error } - // Accumulation in elseif - if (s1 == null) { - } else if (s2 == null) { - val len = s1.length - } else if (s3 == null) { - val len1 = s1.length - val len2 = s2.length - } else { - val len1 = s1.length - val len2 = s2.length - val len3 = s3.length + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } } - // Common idioms - if (s1 == null || s2 == null || s3 == null) { - } else { - val len1: Int = s1.length - val len2: Int = s2.length - val len3: Int = s3.length - } + def commonIdioms() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? - if (s1 != null && s2 != null && s3 != null) { - val len1: Int = s1.length - val len2: Int = s2.length - val len3: Int = s3.length + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } } - // Basic negation - if (!(s1 != null)) { - val len = s1.length // error - } else { - val len = s1.length + def basicNegation() = { + val s1: String|Null = ??? + if (!(s1 != null)) { + val len = s1.length // error + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } } - - if (!(!(!(!(s1 != null))))) { - val len1 = s1.length + + def parens() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } } + + def operatorPrec() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? - // Parens - if ((((s1 == null))) || s2 == null) { - } else { - val len1 = s1.length - val len2 = s2.length + if (s1 != null || s2 != null && s3 != null) { + val len = s3.length // error + } + + if (s1 != null && s2 != null || s3 != null) { + val len1 = s1.length // error + val len2 = s2.length // error + val len3 = s3.length // error + } + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + val len2 = s2.length // error + val len3 = s3.length // error + } } - // Operator precedence - if (s1 != null || s2 != null && s3 != null) { - val len = s3.length // error - } + def insideCond() = { + val x: String|Null = ??? + if (x != null && x.length > 0) { + val len = x.length + } else { + val len = x.length // error + } - if (s1 != null && s2 != null || s3 != null) { - val len1 = s1.length // error - val len2 = s2.length // error - val len3 = s3.length // error - } + if (x == null || x.length > 0) { + val len = x.length // error + } else { + val len = x.length + } - if (s1 != null && (s2 != null || s3 != null)) { - val len1 = s1.length - val len2 = s2.length // error - val len3 = s3.length // error + class Rec { + val r: Rec|Null = ??? + } + + val r: Rec|Null = ??? + if (r != null && r.r != null && (r.r.r == null || r.r.r.r == r)) { + val err = r.r.r.r // error + } } } + diff --git a/tests/pos/explicit-null-flow.scala b/tests/pos/explicit-null-flow.scala index 4e0fc59e4971..326f261afd90 100644 --- a/tests/pos/explicit-null-flow.scala +++ b/tests/pos/explicit-null-flow.scala @@ -2,114 +2,155 @@ // Flow-sensitive type inference class Foo { - class Bar { - val s: String = ??? - } - - // Basic - val b: Bar|Null = ??? - if (b != null) { - val s = b.s // ok: type of `b` inferred as `Bar` - val s2: Bar = b - } else { - } + def basic() = { + class Bar { + val s: String = ??? + } - // Not stable - var b2: Bar|Null = ??? - if (b2 != null) { + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + } } - class Bar2 { - val x: Bar2|Null = ??? - } + def nestedAndSelection() = { + class Bar2 { + val x: Bar2|Null = ??? + } - // Nested and selection - val bar2: Bar2|Null = ??? - if (bar2 != null) { - if (bar2.x != null) { - if (bar2.x.x != null) { - if (bar2.x.x.x != null) { - val b2: Bar2 = bar2.x.x.x + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x } - val b2: Bar2 = bar2.x.x + val b2: Bar2 = bar2.x } - val b2: Bar2 = bar2.x + val b2: Bar2 = bar2 } - val b2: Bar2 = bar2 } - // If-then-else and equality - val s: String|Null = ??? - if (s == null) { - } else { - val len: Int = s.length - val len2 = s.length + def ifThenElse() = { + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } } - // Elseif - val s1: String|Null = ??? - val s2: String|Null = ??? - val s3: String|Null = ??? - if (s1 != null) { - val len = s1.length - } else if (s2 != null) { - val len = s2.length - } else if (s3 != null) { - val len = s3.length - } - // Accumulation in elseif - if (s1 == null) { - } else if (s2 == null) { - val len = s1.length - } else if (s3 == null) { - val len1 = s1.length - val len2 = s2.length - } else { - val len1 = s1.length - val len2 = s2.length - val len3 = s3.length + def elseIf() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + } else if (s2 != null) { + val len = s2.length + } else if (s3 != null) { + val len = s3.length + } + + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } } - // Common idioms - if (s1 == null || s2 == null || s3 == null) { - } else { - val len1: Int = s1.length - val len2: Int = s2.length - val len3: Int = s3.length - } + def commonIdioms() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? - if (s1 != null && s2 != null && s3 != null) { - val len1: Int = s1.length - val len2: Int = s2.length - val len3: Int = s3.length - } + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } - // Basic negation - if (!(s1 != null)) { - } else { - val len = s1.length + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } } - if (!(!(!(!(s1 != null))))) { - val len1 = s1.length + def basicNegation() = { + val s1: String|Null = ??? + + if (!(s1 != null)) { + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } } - // Parens - if ((((s1 == null))) || s2 == null) { - } else { - val len1 = s1.length - val len2 = s2.length + def parens() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } } - // Operator precedence - if (s1 != null || s2 != null && s3 != null) { + def operatorPrecedence() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + } } - if (s1 != null && s2 != null || s3 != null) { + def propInsideCond() = { + val s: String|Null = ??? + if (s != null && s.length > 0) { + val len: Int = s.length + } + + if (s == null || s.length == 0) { + } else { + val len: Int = s.length + } + + class Rec { + val r: Rec|Null = ??? + } + + val r: Rec|Null = ??? + if (r != null && r.r != null && r.r.r != null && (r.r.r.r != null) && r.r.r.r.r != null) { + val r6: Rec|Null = r.r.r.r.r.r + } + + if (r == null || r.r == null || r.r.r == null || (r.r.r.r == null) || r.r.r.r.r == null) { + } else { + val r6: Rec|Null = r.r.r.r.r.r + } } - if (s1 != null && (s2 != null || s3 != null)) { - val len1 = s1.length + def interactWithTypeInference() = { + val f: String|Null => Int = (x) => if (x != null) x.length else 0 } } diff --git a/tests/pos/t1107a.scala b/tests/pos/t1107a.scala index 0e433a97a667..8d77d07d0455 100644 --- a/tests/pos/t1107a.scala +++ b/tests/pos/t1107a.scala @@ -1,5 +1,4 @@ object F { - implicit def stripNull[T](x: T | Null): T = x.asInstanceOf[T] type AnyClass = Class[_] def tryf[T](ignore: List[AnyClass] | Null)(f: => T): Any = { try { From 380ed44cb2b46cff4495349c31255092abee9559 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 7 Dec 2018 14:53:05 -0500 Subject: [PATCH 078/127] Add test case using unary neg --- tests/pos/explicit-null-flow.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/pos/explicit-null-flow.scala b/tests/pos/explicit-null-flow.scala index 326f261afd90..bbe42a923b4d 100644 --- a/tests/pos/explicit-null-flow.scala +++ b/tests/pos/explicit-null-flow.scala @@ -148,6 +148,10 @@ class Foo { } else { val r6: Rec|Null = r.r.r.r.r.r } + + if (!(r == null) && r.r != null) { + val r3: Rec|Null = r.r.r + } } def interactWithTypeInference() = { From 03539f4bbdb343cf702eb11ca85ba6bd47f13787 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 7 Dec 2018 16:14:42 -0500 Subject: [PATCH 079/127] Fix rebase errors --- library/{src-scala3 => src-bootstrapped}/scala/NonNull.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename library/{src-scala3 => src-bootstrapped}/scala/NonNull.scala (100%) diff --git a/library/src-scala3/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala similarity index 100% rename from library/src-scala3/scala/NonNull.scala rename to library/src-bootstrapped/scala/NonNull.scala From 3d8af6a6d5d6c17107ca93f5032f9a7c743730e0 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 7 Dec 2018 18:17:03 -0500 Subject: [PATCH 080/127] Don't generate fallback Eq in Null case Given `x == null`, to decide whether to generate the fallback `Eq` instance, we would - check if x's type and Null can be assumed to be equal via `assumedCanEqual` - if not, then check whether neither of them has its own `Eq` instance However, since `Null` is no longer a subtype of other types, it no longer has any already-defined Eq instances. This means that step 2 above succeeds, meaning that we could compare T <: AnyVal with Null, which (I think) we don't want to do. The fix is to add an extra check between the two above to only allow comparisons to Null if `assumedCanEqual` would've returned true (which only happens for reference types). --- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 5 ++++- tests/neg/equality.scala | 6 ++++++ tests/neg/i1793.scala | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 60158594f82c..6168ac45104f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -724,8 +724,11 @@ trait Implicits { self: Typer => inferImplicit(defn.EqType.appliedTo(tp, tp), EmptyTree, pos).isSuccess def validEqAnyArgs(tp1: Type, tp2: Type)(implicit ctx: Context) = { + lazy val eitherIsNull = tp1.isRef(defn.NullClass) || tp2.isRef(defn.NullClass) List(tp1, tp2).foreach(fullyDefinedType(_, "eqAny argument", pos)) - assumedCanEqual(tp1, tp2) || !hasEq(tp1) && !hasEq(tp2) + // If either of the types is `Null`, then we only want to generate the fallback `Eq` + // the other type is a reference type. + assumedCanEqual(tp1, tp2) || !eitherIsNull && !hasEq(tp1) && !hasEq(tp2) } inferImplicit(formal, EmptyTree, pos)(ctx) match { diff --git a/tests/neg/equality.scala b/tests/neg/equality.scala index 0fdc8910e94b..570be93d935d 100644 --- a/tests/neg/equality.scala +++ b/tests/neg/equality.scala @@ -58,6 +58,7 @@ object equality { 1 == true // error + // TODO(abeln): double-check that this is the behaviour we want /* null == true // OK by eqProxy or eqJBoolSBool true == null // OK by eqSBoolJBool @@ -68,6 +69,11 @@ object equality { true == null // error null == 1 // error 1 == null // error + { + val x: AnyVal = ??? + if (x == null) {} // error + def foo[T <: AnyVal](x: T) = if (x != null) {} // error + } class Fruit diff --git a/tests/neg/i1793.scala b/tests/neg/i1793.scala index ea6d3bcb78c6..e18e7d71f0ff 100644 --- a/tests/neg/i1793.scala +++ b/tests/neg/i1793.scala @@ -1,7 +1,8 @@ object Test { import scala.ref.WeakReference - def unapply[T <: AnyVal](wr: WeakReference[T]): Option[T] = { + def unapply[T <: AnyVal](wr: WeakReference[T]): Option[T] = { // error val x = wr.underlying.get - if (x != null) Some(x) else None // error + if (x != null) Some(x) else None // The failure used to be here, since x had type T <: AnyVal. but now `x: T|Null`, so + // `x != null` is valid. } } From e258fb146a81ec5ee5d46e1eedd04f047701bd98 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Dec 2018 11:16:13 -0500 Subject: [PATCH 081/127] Fix a few tests --- tests/run/array-charSeq.scala | 8 ++++---- tests/run/numbereq.scala | 2 +- tests/run/t4023.scala | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/run/array-charSeq.scala b/tests/run/array-charSeq.scala index 64055c6406ba..d551dd80b906 100644 --- a/tests/run/array-charSeq.scala +++ b/tests/run/array-charSeq.scala @@ -12,17 +12,17 @@ object Test { } 0 until chars.length foreach { i => - println("sub(%s, %s) == '%s'".format(i, chars.length, chars.subSequence(i, chars.length))) - println("sub(%s, %s) == '%s'".format(0, i, chars.subSequence(0, i))) + println("sub(%s, %s) == '%s'".format(i, chars.length, chars.subSequence(i, chars.length).nn)) + println("sub(%s, %s) == '%s'".format(0, i, chars.subSequence(0, i).nn)) } if (chars.length >= 2) - check(chars.subSequence(1, chars.length - 1)) + check(chars.subSequence(1, chars.length - 1).nn) } def main(args: Array[String]): Unit = { while (xs.length > 0) { check(xs) - xs = xs.subSequence(0, xs.length - 1) + xs = xs.subSequence(0, xs.length - 1).nn } } } diff --git a/tests/run/numbereq.scala b/tests/run/numbereq.scala index 770bf50fee15..c7db6ccfaf7b 100644 --- a/tests/run/numbereq.scala +++ b/tests/run/numbereq.scala @@ -23,7 +23,7 @@ object Test { List( // FIXME: breaks ycheck, broken type inferencing. Gets a large union type // instead of list of Number - List(BigDecimal(x, java.math.MathContext.UNLIMITED): Number), + List(BigDecimal(x, java.math.MathContext.UNLIMITED.nn): Number), List(x), if (x.isValidDouble) List(new java.lang.Double(x.toDouble)) else Nil, if (x.isValidFloat) List(new java.lang.Float(x.toFloat)) else Nil, diff --git a/tests/run/t4023.scala b/tests/run/t4023.scala index 68f5ec815995..83b548ac1b85 100644 --- a/tests/run/t4023.scala +++ b/tests/run/t4023.scala @@ -15,9 +15,9 @@ object Test { // sortBy(_.getName) introduces additional classes which we don't want to see in C, // so we call sortBy outside of C. object TestHelper { - val valuesTry1 = C.classes1.sortBy(_.getName) - val valuesTry2 = C.classes2.sortBy(_.getName) - val valuesTry3 = C.classes3.sortBy(_.getName) + val valuesTry1 = C.classes1.sortBy(_.nn.getName.nn) + val valuesTry2 = C.classes2.sortBy(_.nn.getName.nn) + val valuesTry3 = C.classes3.sortBy(_.nn.getName.nn) } def main(args: Array[String]): Unit = { From d47f825e9dfec9851cd47c8c9f96c2f9d5bb19df Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Dec 2018 14:43:00 -0500 Subject: [PATCH 082/127] Fix a bunch of tests --- tests/generic-java-signatures/andTypes.scala | 3 ++- tests/generic-java-signatures/arrayBound.scala | 5 +++-- tests/generic-java-signatures/boundParameters.scala | 5 +++-- tests/generic-java-signatures/boundsInterfaces.scala | 5 +++-- tests/generic-java-signatures/erased.scala | 5 +++-- tests/generic-java-signatures/higherKinded.scala | 3 ++- tests/generic-java-signatures/i3476.scala | 4 ++-- tests/generic-java-signatures/i4248.scala | 2 +- tests/generic-java-signatures/invalidNames.scala | 3 ++- tests/generic-java-signatures/mangledNames.scala | 3 ++- .../generic-java-signatures/primitiveArrayBound.scala | 5 +++-- tests/generic-java-signatures/primitives.scala | 3 ++- tests/generic-java-signatures/simple.scala | 4 ++-- .../simpleBoundParameters.scala | 5 +++-- tests/generic-java-signatures/valueClassBound.scala | 3 ++- tests/generic-java-signatures/wildcards.scala | 5 +++-- tests/run/i3006.scala | 10 +++++----- tests/run/t6488.scala | 2 +- 18 files changed, 44 insertions(+), 31 deletions(-) diff --git a/tests/generic-java-signatures/andTypes.scala b/tests/generic-java-signatures/andTypes.scala index b4557fd56b6f..042b416d0e1d 100644 --- a/tests/generic-java-signatures/andTypes.scala +++ b/tests/generic-java-signatures/andTypes.scala @@ -10,7 +10,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo].getDeclaredMethod("foo").getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/arrayBound.scala b/tests/generic-java-signatures/arrayBound.scala index d4f24e7f7ca6..b6af9fcd9352 100644 --- a/tests/generic-java-signatures/arrayBound.scala +++ b/tests/generic-java-signatures/arrayBound.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _, _, _, _]].getTypeParameters() tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/boundParameters.scala b/tests/generic-java-signatures/boundParameters.scala index aa80e62d6cb2..792e80794b51 100644 --- a/tests/generic-java-signatures/boundParameters.scala +++ b/tests/generic-java-signatures/boundParameters.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _, _]].getTypeParameters() tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/boundsInterfaces.scala b/tests/generic-java-signatures/boundsInterfaces.scala index 2b0daaa5309a..2597f218afb9 100644 --- a/tests/generic-java-signatures/boundsInterfaces.scala +++ b/tests/generic-java-signatures/boundsInterfaces.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/erased.scala b/tests/generic-java-signatures/erased.scala index a9731cc748fb..88f285e51df8 100644 --- a/tests/generic-java-signatures/erased.scala +++ b/tests/generic-java-signatures/erased.scala @@ -4,11 +4,12 @@ object MyErased { object Test { def main(args: Array[String]): Unit = { - val f1 = MyErased.getClass.getMethods.find(_.getName.endsWith("f1")).get + val f1 = MyErased.getClass.getMethods.find(_.nn.getName.endsWith("f1")).get.nn val tParams = f1.getTypeParameters println(f1.toGenericString) tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/higherKinded.scala b/tests/generic-java-signatures/higherKinded.scala index 624f122676fe..887651d43abf 100644 --- a/tests/generic-java-signatures/higherKinded.scala +++ b/tests/generic-java-signatures/higherKinded.scala @@ -5,7 +5,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _, _, _, _]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/i3476.scala b/tests/generic-java-signatures/i3476.scala index 4af0b77b98a5..9404e478d2a8 100644 --- a/tests/generic-java-signatures/i3476.scala +++ b/tests/generic-java-signatures/i3476.scala @@ -1,9 +1,9 @@ object Test { def hasGenericSignature(cls: Class[_], methName: String): Boolean = { - cls.getDeclaredMethods().find(_.getName.contains(methName)) match { + cls.getDeclaredMethods().find(_.nn.getName.contains(methName)) match { case None => throw new NoSuchMethodError(s"No $methName in ${cls.getName}") - case Some(meth) => meth.getTypeParameters.nonEmpty + case Some(meth) => meth.nn.getTypeParameters.nonEmpty } } diff --git a/tests/generic-java-signatures/i4248.scala b/tests/generic-java-signatures/i4248.scala index 91b34cd41a52..e715b6cb17dd 100644 --- a/tests/generic-java-signatures/i4248.scala +++ b/tests/generic-java-signatures/i4248.scala @@ -4,7 +4,7 @@ object Foo { object Test { def main(args: Array[String]): Unit = { - val f1 = Foo.getClass.getMethods.find(_.getName.endsWith("foo")).get + val f1 = Foo.getClass.getMethods.find(_.nn.getName.endsWith("foo")).get.nn println(f1.toGenericString) } } diff --git a/tests/generic-java-signatures/invalidNames.scala b/tests/generic-java-signatures/invalidNames.scala index 9dbf9da3bdb6..21961070bcff 100644 --- a/tests/generic-java-signatures/invalidNames.scala +++ b/tests/generic-java-signatures/invalidNames.scala @@ -4,7 +4,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_]].getTypeParameters() tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/mangledNames.scala b/tests/generic-java-signatures/mangledNames.scala index b57aff3d4018..4a2ad4db3a0b 100644 --- a/tests/generic-java-signatures/mangledNames.scala +++ b/tests/generic-java-signatures/mangledNames.scala @@ -4,7 +4,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_]].getTypeParameters() tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/primitiveArrayBound.scala b/tests/generic-java-signatures/primitiveArrayBound.scala index 21efdbb56c95..d7904f49ce8f 100644 --- a/tests/generic-java-signatures/primitiveArrayBound.scala +++ b/tests/generic-java-signatures/primitiveArrayBound.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName.nn + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/primitives.scala b/tests/generic-java-signatures/primitives.scala index 8f75309096fc..c497e6ce8f2e 100644 --- a/tests/generic-java-signatures/primitives.scala +++ b/tests/generic-java-signatures/primitives.scala @@ -11,7 +11,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _, _, _, _, _, _, _, _]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds().map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds().map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/simple.scala b/tests/generic-java-signatures/simple.scala index b8c3be1bffd9..310f4e8b4004 100644 --- a/tests/generic-java-signatures/simple.scala +++ b/tests/generic-java-signatures/simple.scala @@ -2,6 +2,6 @@ class Foo[T, U, LongerName] object Test { def main(args: Array[String]): Unit = { val typeParams = classOf[Foo[_, _, _]].getTypeParameters - typeParams.foreach(tp => println(tp.getName)) + typeParams.foreach(tp => println(tp.nn.getName)) } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/simpleBoundParameters.scala b/tests/generic-java-signatures/simpleBoundParameters.scala index beb99cba299a..e231cfca5a4c 100644 --- a/tests/generic-java-signatures/simpleBoundParameters.scala +++ b/tests/generic-java-signatures/simpleBoundParameters.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_]].getTypeParameters() tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/generic-java-signatures/valueClassBound.scala b/tests/generic-java-signatures/valueClassBound.scala index 76711dd500a9..fba7b2741a67 100644 --- a/tests/generic-java-signatures/valueClassBound.scala +++ b/tests/generic-java-signatures/valueClassBound.scala @@ -4,7 +4,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds.map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds.map(_.nn.getTypeName).mkString(", ")) } } } diff --git a/tests/generic-java-signatures/wildcards.scala b/tests/generic-java-signatures/wildcards.scala index ee7b71f5b612..b1679d4299e6 100644 --- a/tests/generic-java-signatures/wildcards.scala +++ b/tests/generic-java-signatures/wildcards.scala @@ -3,7 +3,8 @@ object Test { def main(args: Array[String]): Unit = { val tParams = classOf[Foo[_, _, _]].getTypeParameters tParams.foreach { tp => - println(tp.getName + " <: " + tp.getBounds().map(_.getTypeName).mkString(", ")) + val tp1 = tp.nn + println(tp1.getName + " <: " + tp1.getBounds().map(_.nn.getTypeName).mkString(", ")) } } -} \ No newline at end of file +} diff --git a/tests/run/i3006.scala b/tests/run/i3006.scala index 7347bc800356..79b4421f0df1 100644 --- a/tests/run/i3006.scala +++ b/tests/run/i3006.scala @@ -1,25 +1,25 @@ class Foo { def foo() = { - def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn f() } def bar() = { - def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn f() } def baz() = { - def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn f() } } class Bar { def foo() = { - def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn f() } def bar() = { - def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName + def f() = Thread.currentThread.getStackTrace.apply(1).getMethodName.nn f() } } diff --git a/tests/run/t6488.scala b/tests/run/t6488.scala index 559164044242..ec01a94c5cb8 100644 --- a/tests/run/t6488.scala +++ b/tests/run/t6488.scala @@ -31,7 +31,7 @@ object Test { // fork the data spewer, wait for input, then destroy the process def test(): Unit = { - val f = new File(javaHome, "bin").listFiles.sorted filter (_.getName startsWith "java") find (_.canExecute) getOrElse { + val f = new File(javaHome, "bin").listFiles.map(_.nn).sorted filter (_.getName startsWith "java") find (_.canExecute) getOrElse { // todo signal test runner that test is skipped new File("/bin/ls") // innocuous } From 140e52848f4488796d521dd99eed8d288d80307a Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 10 Dec 2018 16:39:55 -0500 Subject: [PATCH 083/127] More robust id of JavaNull alias --- compiler/src/dotty/tools/dotc/core/Types.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 687b98367d1e..15d211f98690 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -263,7 +263,12 @@ object Types { } /** Is this type exactly `JavaNull` (no vars, aliases, refinements etc allowed)? */ - def isJavaNullType(implicit ctx: Context): Boolean = this == defn.JavaNullType + def isJavaNullType(implicit ctx: Context): Boolean = { + // We can't do `this == defn.JavaNull` because when trees are unpickled new references + // to `JavaNull` could be created that are different from `defn.JavaNull`. + // Instead, we compare the symbol. + this.isDirectRef(defn.JavaNull) + } /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ def isJavaNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { From 96f9af4243c359dc244f6d9c446ad05e66f82bc6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 11:28:12 -0500 Subject: [PATCH 084/127] fix tests --- compiler/test/dotc/run-from-tasty.blacklist | 1 - compiler/test/dotc/run-test-pickling.blacklist | 3 +++ tests/neg/inline-case-objects/Macro_1.scala | 2 +- tests/pos/opaque-immutable-array.scala | 2 +- tests/run/hmap.scala | 4 ++-- tests/run/imports.scala | 18 +++++++++--------- tests/run/nothing-var.scala | 2 +- tests/run/t603.scala | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/compiler/test/dotc/run-from-tasty.blacklist b/compiler/test/dotc/run-from-tasty.blacklist index a2fbf589a9c8..1e39bdc96770 100644 --- a/compiler/test/dotc/run-from-tasty.blacklist +++ b/compiler/test/dotc/run-from-tasty.blacklist @@ -8,4 +8,3 @@ puzzle.scala implicitMatch.scala typeclass-derivation1.scala typeclass-derivation2.scala - diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index aa1c42f1a07e..48aa0dbfc8c3 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -6,3 +6,6 @@ t7374 tuples1.scala tuples1a.scala typeclass-derivation2.scala + +# need support for flow-sensitive type inference +constant-optimization.scala diff --git a/tests/neg/inline-case-objects/Macro_1.scala b/tests/neg/inline-case-objects/Macro_1.scala index 95bfeec347bb..a376567d8f48 100644 --- a/tests/neg/inline-case-objects/Macro_1.scala +++ b/tests/neg/inline-case-objects/Macro_1.scala @@ -2,7 +2,7 @@ import scala.quoted._ object Macros { - def impl(foo: Any): Expr[String] = foo.getClass.getCanonicalName.toExpr + def impl(foo: Any): Expr[String] = foo.getClass.getCanonicalName.nn.toExpr } class Bar { diff --git a/tests/pos/opaque-immutable-array.scala b/tests/pos/opaque-immutable-array.scala index 711a8e0a61e8..5e1839a63376 100644 --- a/tests/pos/opaque-immutable-array.scala +++ b/tests/pos/opaque-immutable-array.scala @@ -44,4 +44,4 @@ object ia { } -lower - 1 } -} \ No newline at end of file +} diff --git a/tests/run/hmap.scala b/tests/run/hmap.scala index d84419ce1e8d..ddd48ae9789b 100644 --- a/tests/run/hmap.scala +++ b/tests/run/hmap.scala @@ -43,11 +43,11 @@ trait PhantomGet[K, M <: Tuple, I <: Nat] // extends PhantomAny object PhantomGet { implicit def getHead[K, V, T <: Tuple] - : PhantomGet[K, HEntry[K, V] TCons T, Zero] = null + : PhantomGet[K, HEntry[K, V] TCons T, Zero] = new PhantomGet[K, HEntry[K, V] TCons T, Zero]{} implicit def getTail[K, H, T <: Tuple, I <: Nat] (implicit t: PhantomGet[K, T, I]) - : PhantomGet[K, H TCons T, Succ[I]] = null + : PhantomGet[K, H TCons T, Succ[I]] = new PhantomGet[K, H TCons T, Succ[I]]{} } // Syntax --------------------------------------------------------------------- diff --git a/tests/run/imports.scala b/tests/run/imports.scala index adc173c6af62..6ecc97d84ffb 100644 --- a/tests/run/imports.scala +++ b/tests/run/imports.scala @@ -25,8 +25,8 @@ class C_ico() { o_ico.v_ico = this; import o_ico.v_ico; override def toString(): String = "C_ico"; - def method: C_ico = v_ico; - val field: C_ico = v_ico; + def method: C_ico|Null = v_ico; + val field: C_ico|Null = v_ico; check("C_ico", "v_ico ", v_ico); check("C_ico", "field ", field); @@ -35,14 +35,14 @@ class C_ico() { } object o_ico { - var v_ico: C_ico = null; + var v_ico: C_ico|Null = null; new C_ico(); } //############################################################################ object o_ioc { - var v_ioc: C_ioc = null; + var v_ioc: C_ioc|Null = null; new C_ioc(); } @@ -52,8 +52,8 @@ import o_ioc.v_ioc; class C_ioc() { o_ioc.v_ioc = this; override def toString(): String = "C_ioc"; - def method: C_ioc = v_ioc; - val field: C_ioc = v_ioc; + def method: C_ioc|Null = v_ioc; + val field: C_ioc|Null = v_ioc; check("C_ioc", "v_ioc ", v_ioc); check("C_ioc", "field ", field); @@ -64,7 +64,7 @@ class C_ioc() { //############################################################################ object o_oic { - var v_oic: C_oic = null; + var v_oic: C_oic|Null = null; new C_oic(); } @@ -73,8 +73,8 @@ import o_oic.v_oic; class C_oic() { o_oic.v_oic = this; override def toString(): String = "C_oic"; - def method: C_oic = v_oic; - val field: C_oic = v_oic; + def method: C_oic|Null = v_oic; + val field: C_oic|Null = v_oic; check("C_oic", "v_oic ", v_oic); check("C_oic", "field ", field); diff --git a/tests/run/nothing-var.scala b/tests/run/nothing-var.scala index bb31253439a1..6453c16c7f32 100644 --- a/tests/run/nothing-var.scala +++ b/tests/run/nothing-var.scala @@ -16,7 +16,7 @@ object Test { case e: NotImplementedError => println("???") } - assert(!classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!classOf[Foo].getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } diff --git a/tests/run/t603.scala b/tests/run/t603.scala index 84a224a40a91..f3e2d49ecf8a 100644 --- a/tests/run/t603.scala +++ b/tests/run/t603.scala @@ -2,12 +2,12 @@ object forceDelay { import scala.language.implicitConversions class Susp[+A](lazyValue: => A) extends Function0[A] { - private var func: () => Any = () => lazyValue + private var func: (() => Any)|Null = () => lazyValue private var value: Any = null override def apply() = { if (func != null) { - value = func().asInstanceOf[A] + value = func.nn().asInstanceOf[A] func = null } value.asInstanceOf[A] From 22668210eaa991b7153190fc90324218fe79efdb Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 13:42:01 -0500 Subject: [PATCH 085/127] Teach space checks that reference types are non-nullable --- .../tools/dotc/transform/patmat/Space.scala | 41 +++++++++---------- .../pos/explicit-null-pattern-matching.scala | 33 +++++++++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 tests/pos/explicit-null-pattern-matching.scala diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 42c2580f78c4..959f242245b3 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -289,15 +289,23 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { private val scalaNilType = ctx.requiredModuleRef("scala.collection.immutable.Nil") private val scalaConsType = ctx.requiredClassRef("scala.collection.immutable.::") - private val nullType = ConstantType(Constant(null)) - private val nullSpace = Typ(nullType) + private val constantNullType = ConstantType(Constant(null)) + private val constantNullSpace = Typ(constantNullType) + + /** Does the given tree stand for the literal `null`? */ + def isNullLit(tree: Tree): Boolean = tree match { + case Literal(Constant(null)) => true + case _ => false + } + + /** Does the given space contain just the value `null`? */ + def isNullSpace(space: Space): Boolean = space match { + case Typ(tpe, _) => tpe =:= constantNullType || tpe.isRefToNull + case Or(spaces) => spaces.forall(isNullSpace) + case _ => false + } override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = { - // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1) - if (tp1 == nullType || tp2 == nullType) { - // Since projections of types don't include null, intersection with null is empty. - return Empty - } val and = AndType(tp1, tp2) // Then, no leaf of the and-type tree `and` is a subtype of `and`. val res = inhabited(and) @@ -328,7 +336,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { Typ(ConstantType(c), false) case _: BackquotedIdent => Typ(pat.tpe, false) case Ident(nme.WILDCARD) => - Or(Typ(pat.tpe.stripAnnots, false) :: nullSpace :: Nil) + Or(Typ(pat.tpe.stripAnnots, false) :: constantNullSpace :: Nil) case Ident(_) | Select(_, _) => Typ(pat.tpe.stripAnnots, false) case Alternative(trees) => Or(trees.map(project(_))) @@ -389,7 +397,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { /** Is `tp1` a subtype of `tp2`? */ def isSubType(tp1: Type, tp2: Type): Boolean = { - val res = (tp1 != nullType || tp2 == nullType) && tp1 <:< tp2 + val res = tp1 <:< tp2 debug.println(s"${tp1} <:< ${tp2} = $res") res } @@ -936,11 +944,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { if (!redundancyCheckable(sel)) return - val targetSpace = - if (selTyp.classSymbol.isPrimitiveValueClass) - Typ(selTyp, true) - else - Or(Typ(selTyp, true) :: nullSpace :: Nil) + val targetSpace = Typ(selTyp, true) // in redundancy check, take guard as false in order to soundly approximate def projectPrevCases(cases: List[CaseDef]): Space = @@ -949,11 +953,6 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { else Empty }.reduce((a, b) => Or(List(a, b))) - def isNull(tree: Tree): Boolean = tree match { - case Literal(Constant(null)) => true - case _ => false - } - (1 until cases.length).foreach { i => val prevs = projectPrevCases(cases.take(i)) @@ -977,9 +976,9 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { } // if last case is `_` and only matches `null`, produce a warning - if (i == cases.length - 1 && !isNull(pat) ) { + if (i == cases.length - 1 && !isNullLit(pat) ) { simplify(minus(covered, prevs)) match { - case Typ(`nullType`, _) => + case space if isNullSpace(space) => ctx.warning(MatchCaseOnlyNullWarning(), pat.pos) case _ => } diff --git a/tests/pos/explicit-null-pattern-matching.scala b/tests/pos/explicit-null-pattern-matching.scala new file mode 100644 index 000000000000..e1d0da228114 --- /dev/null +++ b/tests/pos/explicit-null-pattern-matching.scala @@ -0,0 +1,33 @@ + +class Foo { + val s: String = ??? + s match { + case s: String => 100 + case _ => 200 // warning: unreachable + } + + sealed trait Animal + case class Dog(name: String) extends Animal + case object Cat extends Animal + + val a: Animal = ??? + a match { + case Dog(name) => 100 + case Cat => 200 + case _ => 300 // warning: unreachable + } + + val a2: Animal|Null = ??? + a2 match { + case Dog(_) => 100 + case Cat => 200 + case _ => 300 // warning: only matches null + } + + val a3: Animal|Null = ??? + a3 match { + case Dog(_) => 100 + case Cat => 200 + case null => 300 // ok + } +} From 198c631d897d490b61d446e44d7b3648c60f3fff Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 13:57:57 -0500 Subject: [PATCH 086/127] fix tests --- tests/run/exceptions-2.scala | 8 ++++---- tests/run/exceptions-nest.scala | 2 +- tests/run/tryPatternMatch.scala | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/run/exceptions-2.scala b/tests/run/exceptions-2.scala index 8d755c380969..01de752b23db 100644 --- a/tests/run/exceptions-2.scala +++ b/tests/run/exceptions-2.scala @@ -91,8 +91,8 @@ object Test { } try { - val a: Leaf = null; - println(a.x); + val a: Leaf|Null = null; + println(a.nn.x); } catch { case Leaf(a) => Console.println(a); case _: NullPointerException => Console.println("Exception occurred"); @@ -101,8 +101,8 @@ object Test { def method3: Unit = try { try { - val a: Leaf = null; - println(a.x); + val a: Leaf|Null = null; + println(a.nn.x); } catch { case Leaf(a) => Console.println(a); } diff --git a/tests/run/exceptions-nest.scala b/tests/run/exceptions-nest.scala index 87586485c0c9..62255ac05f33 100644 --- a/tests/run/exceptions-nest.scala +++ b/tests/run/exceptions-nest.scala @@ -83,7 +83,7 @@ object Test extends dotty.runtime.LegacyApp { var x = 1 try { x = 2 - (null: String).toString + (null: String|Null).nn.toString } catch { case e: NullPointerException => throw e diff --git a/tests/run/tryPatternMatch.scala b/tests/run/tryPatternMatch.scala index db01ec172288..e1f287a3b910 100644 --- a/tests/run/tryPatternMatch.scala +++ b/tests/run/tryPatternMatch.scala @@ -3,7 +3,7 @@ import java.util.concurrent.TimeoutException object IAE { def unapply(e: Exception): Option[String] = - if (e.isInstanceOf[IllegalArgumentException] && e.getMessage != null) Some(e.getMessage) + if (e.isInstanceOf[IllegalArgumentException] && e.getMessage != null) Some(e.getMessage.nn) else None } From 1e0407ac0352f515277f0c50f920ee5118b71065 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 14:27:38 -0500 Subject: [PATCH 087/127] When collecting nullable fields, use old notion of nullability This is motivated by tests/run/i1692.scala --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 8 ++++++-- .../tools/dotc/transform/CollectNullableFields.scala | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index f9fe6e984d06..08d020e688c5 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -682,11 +682,15 @@ object SymDenotations { /** Is this symbol a class with nullable values? */ final def isNullableClass(implicit ctx: Context): Boolean = { - // After erasure, reference types become nullable again. if (!ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass - else isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass + else isNullableClassAfterErasure } + /** Is this symbol a class with nullable values after erasure? */ + final def isNullableClassAfterErasure(implicit ctx: Context): Boolean = { + // After erasure, reference types become nullable again. + isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass + } /** Is this definition accessible as a member of tree with type `pre`? * @param pre The type of the tree from which the selection is made diff --git a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala index bae91bf62417..f3305ece0a3c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala +++ b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala @@ -34,7 +34,7 @@ object CollectNullableFields { * - belongs to a non trait-class * - is private[this] * - is not lazy - * - its type is nullable + * - its type is nullable after erasure * - is only used in a lazy val initializer * - defined in the same class as the lazy val */ @@ -65,7 +65,9 @@ class CollectNullableFields extends MiniPhase { !sym.is(Lazy) && !sym.owner.is(Trait) && sym.initial.is(PrivateLocal) && - sym.info.widenDealias.typeSymbol.isNullableClass + // We need `isNullableClassAfterErasure` and not `isNullable` because + // we care about the values as present in the JVM. + sym.info.widenDealias.typeSymbol.isNullableClassAfterErasure if (isNullablePrivateField) nullability.get(sym) match { From 94770ae81b24950daf59148b119148206a3598ce Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 14:30:28 -0500 Subject: [PATCH 088/127] fix tests --- tests/run/nothing-lazy-val.scala | 2 +- tests/run/null-var.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run/nothing-lazy-val.scala b/tests/run/nothing-lazy-val.scala index 3e7aada0da2e..4efb4e3765b9 100644 --- a/tests/run/nothing-lazy-val.scala +++ b/tests/run/nothing-lazy-val.scala @@ -14,7 +14,7 @@ object Test { // TODO: Erase // Currently not erasing fields for lazy vals - assert(classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + assert(classOf[Foo].getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") } diff --git a/tests/run/null-var.scala b/tests/run/null-var.scala index 56bca4b9bcde..58c23980d5d8 100644 --- a/tests/run/null-var.scala +++ b/tests/run/null-var.scala @@ -9,7 +9,7 @@ object Test { null } println(f.foo) - assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } } From 5899029c7ca4911712c91068931e8e1f905d025a Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 13 Dec 2018 14:33:25 -0500 Subject: [PATCH 089/127] fix test --- tests/run/t2755.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run/t2755.scala b/tests/run/t2755.scala index 8d10b567346b..fb07d3425d48 100644 --- a/tests/run/t2755.scala +++ b/tests/run/t2755.scala @@ -10,7 +10,7 @@ object Test { case x: Array[_] => 6 case _ => 7 } - def f2(a: Array[_]) = a match { + def f2(a: Array[_]|Null) = a match { case x: Array[Int] => x(0) case x: Array[Double] => 2 case x: Array[Float] => x.sum.toInt @@ -19,7 +19,7 @@ object Test { case x: Array[_] => 6 case _ => 7 } - def f3[T](a: Array[T]) = a match { + def f3[T](a: Array[T]|Null) = a match { case x: Array[Int] => x(0) case x: Array[Double] => 2 case x: Array[Float] => x.sum.toInt From 6f9ba24af61f14db670f49c80c057927159c0c25 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Dec 2018 15:51:30 -0500 Subject: [PATCH 090/127] fix tests --- tests/run/delambdafy-dependent-on-param-subst-2.scala | 4 ++-- tests/run/no-useless-forwarders.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/run/delambdafy-dependent-on-param-subst-2.scala b/tests/run/delambdafy-dependent-on-param-subst-2.scala index 4f7b2dc6e7b4..e89e59439ce6 100644 --- a/tests/run/delambdafy-dependent-on-param-subst-2.scala +++ b/tests/run/delambdafy-dependent-on-param-subst-2.scala @@ -13,8 +13,8 @@ object Test { // (new O: M[Null]).m(null) // Okay ((a: A) => { - class N extends M[a.C] { def m(x: a.C) = true } + class N extends M[a.C|Null] { def m(x: a.C|Null) = true } new N: M[Null] - }).apply(a).m(null) // NPE, missing bridge + }).apply(a).m(null) // This used to be incorrectly marked as throwing an NPE. } } diff --git a/tests/run/no-useless-forwarders.scala b/tests/run/no-useless-forwarders.scala index 699295027300..c65cd82c6ca9 100644 --- a/tests/run/no-useless-forwarders.scala +++ b/tests/run/no-useless-forwarders.scala @@ -9,9 +9,9 @@ trait B { object Test extends A with B{ def main(args: Array[String]) = { - assert(!this.getClass.getDeclaredMethods.exists{x: java.lang.reflect.Method => x.getName == "foo"}, + assert(!this.getClass.getDeclaredMethods.exists{x: java.lang.reflect.Method|Null => x.nn.getName == "foo"}, "no forwarder is needed here") - assert(!this.getClass.getDeclaredMethods.exists{x: java.lang.reflect.Method => x.getName == "bar"}, + assert(!this.getClass.getDeclaredMethods.exists{x: java.lang.reflect.Method|Null => x.nn.getName == "bar"}, "no forwarder is needed here") } } From a047492f0e4bec23e63f3cc6c68311aac719fbf2 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Dec 2018 16:55:16 -0500 Subject: [PATCH 091/127] fix tests --- tests/run/gestalt-optional-staging/Test_2.scala | 12 ++++++------ tests/run/t5604.scala | 2 +- tests/run/t8233-bcode.scala | 2 +- tests/run/value-class-extractor-2.scala | 2 +- tests/run/value-class-partial-func-depmet.scala | 5 ++++- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/run/gestalt-optional-staging/Test_2.scala b/tests/run/gestalt-optional-staging/Test_2.scala index 039091974350..239c6a8f85fc 100644 --- a/tests/run/gestalt-optional-staging/Test_2.scala +++ b/tests/run/gestalt-optional-staging/Test_2.scala @@ -16,18 +16,18 @@ object Test { } def getOrElseTest(): Unit = { - val opt = new Optional[String]("hello") + val opt = new Optional[String|Null]("hello") assert(opt.getOrElse("world") == "hello") - val opt2 = new Optional[String](null) + val opt2 = new Optional[String|Null](null) assert(opt2.getOrElse("hello") == "hello") } def mapTest(): Unit = { - val opt = new Optional[String]("hello") + val opt = new Optional[String|Null]("hello") assert(opt.map(_ + " world") == new Optional("hello world")) - val opt2 = new Optional[String](null) + val opt2 = new Optional[String|Null](null) assert(opt2.map(_ + " world") == new Optional(null)) } @@ -64,7 +64,7 @@ object Test { } def `owner chain corruptionTest`(): Unit = { - def foo(x: => Optional[C]) = x + def foo(x: => Optional[C|Null]) = x foo({ val y = new Optional(null); y }).getOrElse(new C) } @@ -74,7 +74,7 @@ object Test { } def `the final thingTest`(): Unit = { - def foo(f: => C): C = f + def foo(f: => C|Null): C|Null = f val x1 = new Optional(new C) val x2 = x1.map(x => foo({ val y = x; y })) } diff --git a/tests/run/t5604.scala b/tests/run/t5604.scala index eccad1639bde..2b55af130c4e 100644 --- a/tests/run/t5604.scala +++ b/tests/run/t5604.scala @@ -29,7 +29,7 @@ package foo { } package bar { object Main { - def main(args:Array[String]): Unit = { + def main(args:Array[String]|Null): Unit = { duh(33L) duh(3.0d) foo.bar.duh(33L) diff --git a/tests/run/t8233-bcode.scala b/tests/run/t8233-bcode.scala index 771e4cf0c1b0..e4543a7604bb 100644 --- a/tests/run/t8233-bcode.scala +++ b/tests/run/t8233-bcode.scala @@ -1,5 +1,5 @@ object Test { - def bar(s: String) = s; + def bar(s: String|Null) = s; val o: Option[Null] = None def nullReference: Unit = { val a: Null = o.get diff --git a/tests/run/value-class-extractor-2.scala b/tests/run/value-class-extractor-2.scala index 5850d42f037e..22dc9952b357 100644 --- a/tests/run/value-class-extractor-2.scala +++ b/tests/run/value-class-extractor-2.scala @@ -43,7 +43,7 @@ object ValueOpt { // 69: astore 5 // 71: aload 5 // 73: areturn - def unapply(x: Any): Opt[String] = x match { + def unapply(x: Any): Opt[String|Null] = x match { case _: String => Opt("String") case _: List[_] => Opt("List") case _: Int => Opt("Int") diff --git a/tests/run/value-class-partial-func-depmet.scala b/tests/run/value-class-partial-func-depmet.scala index f8d2a16e73c5..4ba56adba10c 100644 --- a/tests/run/value-class-partial-func-depmet.scala +++ b/tests/run/value-class-partial-func-depmet.scala @@ -1,5 +1,8 @@ class C -class A { class C } +class A { + type C = C1|Null + class C1 +} object Test { def main(args: Array[String]): Unit = { From 03435d53184103872da5d074cf9992862449a6b1 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 17 Dec 2018 16:57:53 -0500 Subject: [PATCH 092/127] fix tests --- tests/run/unit-var.scala | 2 +- tests/run/unit_erasure.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/run/unit-var.scala b/tests/run/unit-var.scala index c024813a887f..703a5d0b4f00 100644 --- a/tests/run/unit-var.scala +++ b/tests/run/unit-var.scala @@ -8,7 +8,7 @@ object Test { println("foo3") } f.foo - assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + assert(!f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "field foo not erased") } } diff --git a/tests/run/unit_erasure.scala b/tests/run/unit_erasure.scala index 51ccf0a16668..0a75772c8f4d 100644 --- a/tests/run/unit_erasure.scala +++ b/tests/run/unit_erasure.scala @@ -9,8 +9,8 @@ class A { object Test { def main(args: Array[String]): Unit = { - classOf[A].getMethods.toList.filter(_.getName.startsWith("foo")).foreach { m => - assert(m.getGenericReturnType == Void.TYPE, s"Method does not return void: `${m}`") + classOf[A].getMethods.toList.filter(_.nn.getName.startsWith("foo")).foreach { m => + assert(m.nn.getGenericReturnType == Void.TYPE, s"Method does not return void: `${m}`") } } -} \ No newline at end of file +} From 6ef2d32691ec8c9a6c3eba60f1d25a2a70cefcf1 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 11:39:46 -0500 Subject: [PATCH 093/127] fix test --- tests/run/gestalt-optional-inline/Test_2.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/run/gestalt-optional-inline/Test_2.scala b/tests/run/gestalt-optional-inline/Test_2.scala index 039091974350..239c6a8f85fc 100644 --- a/tests/run/gestalt-optional-inline/Test_2.scala +++ b/tests/run/gestalt-optional-inline/Test_2.scala @@ -16,18 +16,18 @@ object Test { } def getOrElseTest(): Unit = { - val opt = new Optional[String]("hello") + val opt = new Optional[String|Null]("hello") assert(opt.getOrElse("world") == "hello") - val opt2 = new Optional[String](null) + val opt2 = new Optional[String|Null](null) assert(opt2.getOrElse("hello") == "hello") } def mapTest(): Unit = { - val opt = new Optional[String]("hello") + val opt = new Optional[String|Null]("hello") assert(opt.map(_ + " world") == new Optional("hello world")) - val opt2 = new Optional[String](null) + val opt2 = new Optional[String|Null](null) assert(opt2.map(_ + " world") == new Optional(null)) } @@ -64,7 +64,7 @@ object Test { } def `owner chain corruptionTest`(): Unit = { - def foo(x: => Optional[C]) = x + def foo(x: => Optional[C|Null]) = x foo({ val y = new Optional(null); y }).getOrElse(new C) } @@ -74,7 +74,7 @@ object Test { } def `the final thingTest`(): Unit = { - def foo(f: => C): C = f + def foo(f: => C|Null): C|Null = f val x1 = new Optional(new C) val x2 = x1.map(x => foo({ val y = x; y })) } From 5ab29f1c867679b4fc83f867fa8ed68c5ab7a97b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 11:53:42 -0500 Subject: [PATCH 094/127] Make .nn throw a NPE if the underlying value is null --- library/src-bootstrapped/scala/NonNull.scala | 6 +++++- tests/run/explicit-null-nn.scala | 12 ++++++++++++ tests/run/t8601b.scala | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 tests/run/explicit-null-nn.scala diff --git a/library/src-bootstrapped/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala index db9c95a3e40a..5e383141e072 100644 --- a/library/src-bootstrapped/scala/NonNull.scala +++ b/library/src-bootstrapped/scala/NonNull.scala @@ -2,6 +2,10 @@ package scala object NonNull { implicit class NonNull[T](x: T|Null) { - def nn: T = x.asInstanceOf[T] + def nn: T = if (x == null) { + throw new NullPointerException("tried cast away nullability, but value is null") + } else { + x.asInstanceOf[T] + } } } diff --git a/tests/run/explicit-null-nn.scala b/tests/run/explicit-null-nn.scala new file mode 100644 index 000000000000..30ce120e8342 --- /dev/null +++ b/tests/run/explicit-null-nn.scala @@ -0,0 +1,12 @@ +// Check that calling `.nn` on a null value throws a NPE. +object Test { + def len(x: Array[String]|Null): Unit = x.nn.length + def load(x: Array[String]|Null): Unit = x.nn(0) + + def check(x: => Any) = try { x; sys.error("failed to throw NPE!") } catch { case _: NullPointerException => } + + def main(args: Array[String]): Unit = { + check(len(null)) + check(load(null)) + } +} diff --git a/tests/run/t8601b.scala b/tests/run/t8601b.scala index 3816e0b83f31..f8659adc3eaf 100644 --- a/tests/run/t8601b.scala +++ b/tests/run/t8601b.scala @@ -1,6 +1,6 @@ object Test { - def len(x: Array[String]): Unit = x.length - def load(x: Array[String]): Unit = x(0) + def len(x: Array[String]|Null): Unit = x.nn.length + def load(x: Array[String]|Null): Unit = x.nn(0) def newarray(i: Int): Unit = new Array[Int](i) def check(x: => Any) = try { x; sys.error("failed to throw NPE!") } catch { case _: NullPointerException => } From 774e481a09c8829b3939afd8825d134d76f68966 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 14:40:37 -0500 Subject: [PATCH 095/127] fix test --- tests/run/mixin-bridge-methods.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/mixin-bridge-methods.scala b/tests/run/mixin-bridge-methods.scala index e0340ebb125a..bc6df0f2b137 100644 --- a/tests/run/mixin-bridge-methods.scala +++ b/tests/run/mixin-bridge-methods.scala @@ -9,6 +9,6 @@ class Sub extends Foo { object Test { def main(args: Array[String]): Unit = { val ms = classOf[Sub].getDeclaredMethods - assert(ms forall (x => !x.isBridge), ms mkString " ") + assert(ms forall (x => !x.nn.isBridge), ms mkString " ") } } From b89a54f51a21949b5603c41283e7f64368555290 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 14:41:05 -0500 Subject: [PATCH 096/127] Add implicit conversions for nullable arrays --- library/src-bootstrapped/scala/NonNull.scala | 6 ++++++ tests/run/t3452a/S_3.scala | 1 + 2 files changed, 7 insertions(+) diff --git a/library/src-bootstrapped/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala index 5e383141e072..1ce538daf872 100644 --- a/library/src-bootstrapped/scala/NonNull.scala +++ b/library/src-bootstrapped/scala/NonNull.scala @@ -8,4 +8,10 @@ object NonNull { x.asInstanceOf[T] } } + + object ArrayConversions { + // TODO(abeln) add additional versions of these a la FunctionN? + implicit def toNullable1[T](a: Array[T]): Array[T|Null] = a.asInstanceOf[Array[T|Null]] + implicit def toNullable2[T](a: Array[Array[T]]): Array[Array[T|Null]|Null] = a.asInstanceOf[Array[Array[T|Null]|Null]] + } } diff --git a/tests/run/t3452a/S_3.scala b/tests/run/t3452a/S_3.scala index aaa898dcde94..73adb7976f75 100644 --- a/tests/run/t3452a/S_3.scala +++ b/tests/run/t3452a/S_3.scala @@ -1,5 +1,6 @@ object Test { def main(args: Array[String]): Unit = { + import scala.NonNull.ArrayConversions._ J_2.main(args) } } From 62a3c470f22127ac8a9b63f52d6942fa4cc5847b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 15:13:55 -0500 Subject: [PATCH 097/127] fix tests --- tests/pos/opaque-immutable-array.scala | 2 +- tests/run/Course-2002-09.scala | 2 +- tests/run/Course-2002-13.scala | 2 +- tests/run/generic/Enum.scala | 6 +++--- tests/run/generic/Serialization.scala | 2 +- tests/run/hmap-covariant.scala | 4 ++-- tests/run/t6443b.scala | 7 ++++--- tests/run/t8601c.scala | 4 ++-- 8 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/pos/opaque-immutable-array.scala b/tests/pos/opaque-immutable-array.scala index 5e1839a63376..95a533127143 100644 --- a/tests/pos/opaque-immutable-array.scala +++ b/tests/pos/opaque-immutable-array.scala @@ -13,7 +13,7 @@ object ia { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, ia.length) + val arr = Arrays.copyOf(ia, ia.length).nn scala.util.Sorting.quickSort(arr) arr } diff --git a/tests/run/Course-2002-09.scala b/tests/run/Course-2002-09.scala index 0aa6209a2951..a544426e79f8 100644 --- a/tests/run/Course-2002-09.scala +++ b/tests/run/Course-2002-09.scala @@ -94,7 +94,7 @@ class Probe(name: String, q: Quantity) extends Constraint { class Quantity() { private var value: Option[Double] = None; private var constraints: List[Constraint] = List(); - private var informant: Constraint = null; + private var informant: Constraint|Null = null; def getValue: Option[Double] = value; diff --git a/tests/run/Course-2002-13.scala b/tests/run/Course-2002-13.scala index 774695b6c406..9d46d4243dc5 100644 --- a/tests/run/Course-2002-13.scala +++ b/tests/run/Course-2002-13.scala @@ -190,7 +190,7 @@ class Parser(s: String) { def term: Term = { val ch = token.charAt(0); if ('A' <= ch && ch <= 'Z') { val a = token; token = it.next; Var(a) } - else if (it.isDelimiter(ch)) { syntaxError("term expected"); null } + else if (it.isDelimiter(ch)) { syntaxError("term expected"); ??? } else constructor } diff --git a/tests/run/generic/Enum.scala b/tests/run/generic/Enum.scala index ce64ac054933..582072f748ef 100644 --- a/tests/run/generic/Enum.scala +++ b/tests/run/generic/Enum.scala @@ -7,7 +7,7 @@ trait Enum { object runtime { class EnumValues[E <: Enum] { private[this] var myMap: Map[Int, E] = Map() - private[this] var fromNameCache: Map[String, E] = null + private[this] var fromNameCache: Map[String, E]|Null = null def register(v: E) = { require(!myMap.contains(v.enumTag)) @@ -18,8 +18,8 @@ object runtime { def fromInt: Map[Int, E] = myMap def fromName: Map[String, E] = { if (fromNameCache == null) fromNameCache = myMap.values.map(v => v.toString -> v).toMap - fromNameCache + fromNameCache.nn } def values: Iterable[E] = myMap.values } -} \ No newline at end of file +} diff --git a/tests/run/generic/Serialization.scala b/tests/run/generic/Serialization.scala index a82d6bc7a100..56f0e2b8bfe2 100644 --- a/tests/run/generic/Serialization.scala +++ b/tests/run/generic/Serialization.scala @@ -46,7 +46,7 @@ object Serialization { implicit val StringSerializable: Serializable[String] = new Serializable[String] { def write(x: String, out: DataOutputStream) = out.writeUTF(x) - def read(in: DataInputStream) = in.readUTF() + def read(in: DataInputStream) = in.readUTF().nn } def RecSerializable[T, U](implicit diff --git a/tests/run/hmap-covariant.scala b/tests/run/hmap-covariant.scala index 475cc6ee66fe..8bf4a6de7256 100644 --- a/tests/run/hmap-covariant.scala +++ b/tests/run/hmap-covariant.scala @@ -43,11 +43,11 @@ trait PhantomGet[K, M <: Tuple, I <: Nat] // extends PhantomAny object PhantomGet { implicit def getHead[K, V, T <: Tuple] - : PhantomGet[K, HEntry[K, V] TCons T, Zero] = null + : PhantomGet[K, HEntry[K, V] TCons T, Zero] = new PhantomGet[K, HEntry[K, V] TCons T, Zero] {} implicit def getTail[K, H, T <: Tuple, I <: Nat] (implicit t: PhantomGet[K, T, I]) - : PhantomGet[K, H TCons T, Succ[I]] = null + : PhantomGet[K, H TCons T, Succ[I]] = new PhantomGet[K, H TCons T, Succ[I]] {} } // Syntax --------------------------------------------------------------------- diff --git a/tests/run/t6443b.scala b/tests/run/t6443b.scala index 7633bedfc3fc..bb507f26d48c 100644 --- a/tests/run/t6443b.scala +++ b/tests/run/t6443b.scala @@ -1,16 +1,17 @@ trait A { - type D >: Null <: C + type D >: Null <: C|Null def foo(d: D)(d2: d.type): Unit trait C { def bar: Unit = foo(null)(null) } } object B extends A { - class D extends C + class D1 extends C + type D = D1|Null def foo(d: D)(d2: d.type): Unit = () // Bridge method required here! } object Test extends dotty.runtime.LegacyApp { - new B.D().bar + new B.D1().bar } diff --git a/tests/run/t8601c.scala b/tests/run/t8601c.scala index 946a4d4b69b1..6120262fc3ec 100644 --- a/tests/run/t8601c.scala +++ b/tests/run/t8601c.scala @@ -1,6 +1,6 @@ object Test { - def loadField(x: scala.runtime.IntRef): Unit = x.elem - def storeField(x: scala.runtime.IntRef): Unit = x.elem = 42 + def loadField(x: scala.runtime.IntRef|Null): Unit = x.nn.elem + def storeField(x: scala.runtime.IntRef|Null): Unit = x.nn.elem = 42 def check(x: => Any) = try { x; sys.error("failed to throw NPE!") } catch { case _: NullPointerException => } From ed2572942092a23357bcf944b264e1cfb96893ec Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 16:07:30 -0500 Subject: [PATCH 098/127] fix tests --- tests/run/bigDecimalCache.scala | 2 +- tests/run/t3452h.scala | 5 +++-- tests/run/t6706.scala | 4 ++-- tests/run/t7214.scala | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/run/bigDecimalCache.scala b/tests/run/bigDecimalCache.scala index c0c709a50f7d..d7748d888edf 100644 --- a/tests/run/bigDecimalCache.scala +++ b/tests/run/bigDecimalCache.scala @@ -1,7 +1,7 @@ object Test { def main(args: Array[String]): Unit = { val bd5a = BigDecimal(5) - val mc = java.math.MathContext.DECIMAL32 + val mc = java.math.MathContext.DECIMAL32.nn val bd5b = BigDecimal(5,mc) assert(bd5b.mc == mc) diff --git a/tests/run/t3452h.scala b/tests/run/t3452h.scala index d61fb065ba99..2f970d58a9b5 100644 --- a/tests/run/t3452h.scala +++ b/tests/run/t3452h.scala @@ -1,12 +1,13 @@ class Mix extends Foo with Bar { f; } trait T abstract class Foo { - class I extends T + type I = I1|Null + class I1 extends T def f: I f } trait Bar { - type I >: Null <: T + type I >: Null <: T|Null def f: I = null f def gobble: I = null diff --git a/tests/run/t6706.scala b/tests/run/t6706.scala index 905494ca8dc1..0c3a0f005e23 100644 --- a/tests/run/t6706.scala +++ b/tests/run/t6706.scala @@ -1,6 +1,6 @@ object Test { - var name = "foo" + 1 - var s1 = Symbol(name) + var name: String|Null = "foo" + 1 + var s1: Symbol|Null = Symbol(name.nn) s1 = null System.gc val s2 = Symbol("foo1") diff --git a/tests/run/t7214.scala b/tests/run/t7214.scala index d73fe6dd5f8f..3496812d8ab0 100644 --- a/tests/run/t7214.scala +++ b/tests/run/t7214.scala @@ -35,7 +35,7 @@ class Crash { type CdotT = c.T type C2dotT = c2.T - val outerField = t.getClass.getDeclaredFields.find(_.getName contains ("outer")).get + val outerField = t.getClass.getDeclaredFields.find(_.nn.getName contains ("outer")).get.nn outerField.setAccessible(true) (t: Any) match { From fedb58708120660c34d2fa14ec497af57f1bc430 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 18 Dec 2018 16:09:59 -0500 Subject: [PATCH 099/127] fix test --- tests/run/t5610a.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run/t5610a.scala b/tests/run/t5610a.scala index 3787c098455d..362e288eafe7 100644 --- a/tests/run/t5610a.scala +++ b/tests/run/t5610a.scala @@ -1,13 +1,13 @@ object Test extends dotty.runtime.LegacyApp { - class Result(_str: => String) { + class Result(_str: => String|Null) { lazy val str = _str } - def foo(str: => String)(i: Int) = new Result(str) + def foo(str: => String|Null)(i: Int) = new Result(str) def bar(f: Int => Result) = f(42) - var test: String = null + var test: String|Null = null val result = bar(foo(test)) test = "bar" From 00c893e8097abb6107991c4b0f08bd7b62e7ac77 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 19 Dec 2018 17:06:33 -0500 Subject: [PATCH 100/127] fix test --- tests/run/i5067b.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/i5067b.scala b/tests/run/i5067b.scala index 36ff84478248..23c67db8a131 100644 --- a/tests/run/i5067b.scala +++ b/tests/run/i5067b.scala @@ -14,7 +14,7 @@ object Test { case null => try { null match { - case Some(_) => () + case _ if false => () } } catch { case e: MatchError => println("match error nested") From c9d51f74fe02da1bc397a833c392085c35e6cee9 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 19 Dec 2018 18:00:33 -0500 Subject: [PATCH 101/127] Better handling of SAM types This refactors the SAM-related logic so that for all `Closure` objects, the type of the closure is really either a function type or a `SAMType(methodTpe)`. i.e. this makes the handling of SAM types more regular. Additionally, this fixes a bunch of cases where we couldn't pattern match a function literal against a trait|Null. Plus it adds a test case for sam types that actually runs, as opposed to just compiles. --- .../tools/dotc/config/JavaPlatform.scala | 3 +- .../src/dotty/tools/dotc/core/Types.scala | 3 +- .../tools/dotc/transform/ExpandSAMs.scala | 41 +++-- tests/run/enum-approx.scala | 6 +- tests/run/sams.scala | 148 ++++++++++++++++++ 5 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 tests/run/sams.scala diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index cbcf3c703f1d..2ef903b4a271 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -43,7 +43,8 @@ class JavaPlatform extends Platform { cls.superClass == defn.ObjectClass && cls.directlyInheritedTraits.forall(_.is(NoInits)) && !ExplicitOuter.needsOuterIfReferenced(cls) && - cls.typeRef.fields.isEmpty // Superaccessors already show up as abstract methods here, so no test necessary + cls.typeRef.fields.isEmpty && + !cls.typeRef.abstractTermMembers.exists(_.symbol.isSuperAccessor) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 15d211f98690..0b42ba6a3f17 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -33,6 +33,7 @@ import scala.util.hashing.{ MurmurHash3 => hashing } import config.Printers.{core, typr} import reporting.trace import java.lang.ref.WeakReference +import dotty.tools.dotc.transform.SymUtils._ import scala.annotation.internal.sharable @@ -4368,7 +4369,7 @@ object Types { // See tests/pos/explicit-null-sam-types.scala val strippedTp = tp.stripNull if (isInstantiatable(strippedTp)) { - val absMems = strippedTp.abstractTermMembers + val absMems = strippedTp.abstractTermMembers.filter(!_.symbol.isSuperAccessor) // println(s"absMems: ${absMems map (_.show) mkString ", "}") if (absMems.size == 1) absMems.head.info match { diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index 7c28638c2ebe..572bad944a9e 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -10,7 +10,7 @@ import dotty.tools.dotc.reporting.diagnostic.messages.TypeMismatch import dotty.tools.dotc.util.Positions.Position /** Expand SAM closures that cannot be represented by the JVM as lambdas to anonymous classes. - * These fall into five categories + * These fall into six categories * * 1. Partial function closures, we need to generate isDefinedAt and applyOrElse methods for these. * 2. Closures implementing non-trait classes @@ -35,20 +35,33 @@ class ExpandSAMs extends MiniPhase { case Block(stats @ (fn: DefDef) :: Nil, Closure(_, fnRef, tpt)) if fnRef.symbol == fn.symbol => tpt.tpe match { case NoType => - tree // it's a plain function - case tpe if defn.isImplicitFunctionType(tpe) => - tree - case tpe @ SAMType(_) if tpe.stripNull.isRef(defn.PartialFunctionClass) => - val tpe1 = checkRefinements(tpe, fn.pos) - toPartialFunction(tree, tpe1) - case tpe @ SAMType(_) if isPlatformSam(tpe.stripNull.classSymbol.asClass) => - checkRefinements(tpe, fn.pos) - tree - case tpe => - val tpe1 = checkRefinements(tpe, fn.pos) - val Seq(samDenot) = tpe1.abstractTermMembers.filter(!_.symbol.isSuperAccessor) - cpy.Block(tree)(stats, + tree // it's a plain or implicit function + case SAMType(_) => + // If the type is a SAM type, it's also possibly nullable. However, here we're desugaring an + // expression of the form `def fun() = {...}; closure(fun)`, so the desugaring should use the + // more precise non-nullable type for the literal. + // TODO(abeln): this makes handling closures typed as nullable go through the slow path that doesn't + // use SAM types. + // In other words, `val x: Int => Int = (x) => x` and `val x: (Int => Int)|Null = (x) => x` both typecheck, + // but the former uses the platform SAM types, vs the latter which uses an anonymous class. + val tpe = tpt.tpe.stripNull + // TODO(abeln): remove comment below after review. + // The lines below were commented out because implicit function types should be handled + // in `case NoType` above, as per the comment on the contents of `tpe` in the definition of `Closure`. + // if (defn.isImplicitFunctionType(tpe)) { + // tree + if (tpe.isRef(defn.PartialFunctionClass)) { + val tpe1 = checkRefinements(tpe, fn.pos) + toPartialFunction(tree, tpe1) + } else if (isPlatformSam(tpe.classSymbol.asClass)) { + checkRefinements(tpe, fn.pos) + tree + } else { + val tpe1 = checkRefinements(tpe, fn.pos) + val Seq(samDenot) = tpe1.abstractTermMembers.filter(!_.symbol.isSuperAccessor) + cpy.Block(tree)(stats, AnonClass(tpe1 :: Nil, fn.symbol.asTerm :: Nil, samDenot.symbol.asTerm.name :: Nil)) + } } case _ => tree diff --git a/tests/run/enum-approx.scala b/tests/run/enum-approx.scala index f18af97df40a..b6a06165ff21 100644 --- a/tests/run/enum-approx.scala +++ b/tests/run/enum-approx.scala @@ -1,5 +1,5 @@ enum Fun[-T, +U >: Null] { - def f: T => U = this match { + def f: (T => U)|Null = this match { case Identity(g) => g case ConstNull => (_ => null) case ConstNullClass() => (_ => null) @@ -14,8 +14,8 @@ enum Fun[-T, +U >: Null] { object Test { def main(args: Array[String]) = { - val x: Null = Fun.ConstNull.f("abc") - val y: Null = Fun.ConstNullClass().f("abc") + val x: Null = Fun.ConstNull.f.nn("abc") + val y: Null = Fun.ConstNullClass().f.nn("abc") assert(Fun.ConstNullSimple.f == null) } } diff --git a/tests/run/sams.scala b/tests/run/sams.scala new file mode 100644 index 000000000000..bc076f4bcc87 --- /dev/null +++ b/tests/run/sams.scala @@ -0,0 +1,148 @@ +trait X { def foo(x: Int): Int; def bar = foo(2) } + +trait T { + var f = 2 + def foo(x: Int): Int +} + +trait U extends T + +trait V extends Exception { def foo(x: Int): Int } + +trait Y extends X { + def baz = super.bar +} + +trait Z { + def foo(x: Int): Int; 42 +} +trait ZZ extends Z + +abstract class C { + def foo(x: Int): Int + trait I { def foo(x: Int): Int } +} + +object Test { + + + def main(args: Array[String]): Unit = { + { + val x: X = (x: Int) => 2 // should be a closure + assert(x.foo(100) == 2) + } + + { + val t: T = (x: Int) => 2 // needs to be an anonymous class because of defined field + assert(t.foo(200) == 2) + } + + { + val u: U = (x: Int) => 2 // needs to be an anonymous class because of inherited field + assert(u.foo(100) == 2) + } + + { + val v: V = (x: Int) => 2 // needs to be an anonymous class because the trait extends a non-object class + assert(v.foo(100) == 2) + } + + { + val y: Y = (x: Int) => 2 // needs to be an anonymous class because of super accessor + assert(y.foo(100) == 2) + } + + { + val z: Z = (x: Int) => 2 // needs to be an anonymous class because trait has initialization code + val zz: ZZ = (x: Int) => 3 // needs to be an anonymous class becaiuse trait has initialization code + + assert(z.foo(100) == 2) + assert(zz.foo(100) == 3) + } + + { + val c: C = (x: Int) => 2 // needs to be an anonymous class because C is not a trait + val ci: c.I = (x: Int) => 2 // needs to be an anonymous class because it needs an outer pointer + + assert(c.foo(100) == 2) + assert(ci.foo(100) == 2) + } + + // Partial functions + val pf: PartialFunction[Int, Int] = { + case 1 => 1 + case 2 => 2 + } + + val qf: PartialFunction[(Int, String), Int] = { + case (1, "abc") => 1 + case _ => 2 + } + + val rf: PartialFunction[(Int, AnyRef), Int] = { + case (_: Int, _: String) => 1 + case _ => 2 + } + + val sf: PartialFunction[Any, Int] = { + case x: String if x == "abc" => 1 + } + + assert(pf(1) == 1) + assert(qf((1, "abc")) == 1) + assert(rf(1, "hello") == 1) + assert(sf("abc") == 1) + + // With null + { + val f: (Int => Int)|Null = (x: Int) => 2 + assert(f.nn(100) == 2) + } + + { + val pf: PartialFunction[Int, Int]|Null = { + case 1 => 1 + } + assert(pf.nn(1) == 1) + } + + { + val x: X|Null = (x: Int) => 2 + assert(x.nn.foo(100) == 2) + } + + { + val t: T|Null = (x: Int) => 2 + assert(t.nn.foo(200) == 2) + } + + { + val u: U|Null = (x: Int) => 2 + assert(u.nn.foo(100) == 2) + } + + { + val v: V|Null = (x: Int) => 2 + assert(v.nn.foo(100) == 2) + } + + { + val y: Y|Null = (x: Int) => 2 + assert(y.nn.foo(100) == 2) + } + + { + val z: Z|Null = (x: Int) => 2 + val zz: ZZ|Null = (x: Int) => 3 + + assert(z.nn.foo(100) == 2) + assert(zz.nn.foo(100) == 3) + } + + { + val c: C|Null = (x: Int) => 2 + assert(c.nn.foo(100) == 2) + } + } +} + From 48fef33cb467b370418be89f4be415c5b91986ce Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Wed, 19 Dec 2018 18:05:03 -0500 Subject: [PATCH 102/127] add comment --- compiler/src/dotty/tools/dotc/core/Types.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 0b42ba6a3f17..d61186e1c6d1 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4369,6 +4369,7 @@ object Types { // See tests/pos/explicit-null-sam-types.scala val strippedTp = tp.stripNull if (isInstantiatable(strippedTp)) { + // Ignore the synthetic super accessor: it'll be dealt with in `ExpandSAMs`. val absMems = strippedTp.abstractTermMembers.filter(!_.symbol.isSuperAccessor) // println(s"absMems: ${absMems map (_.show) mkString ", "}") if (absMems.size == 1) From 69811e5184e71897bde0fa80c99cb901f2ab3da6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 10:16:49 -0500 Subject: [PATCH 103/127] Add implicit conversions from nullable array --- library/src-bootstrapped/scala/NonNull.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/src-bootstrapped/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala index 1ce538daf872..8b73f59bc46b 100644 --- a/library/src-bootstrapped/scala/NonNull.scala +++ b/library/src-bootstrapped/scala/NonNull.scala @@ -13,5 +13,8 @@ object NonNull { // TODO(abeln) add additional versions of these a la FunctionN? implicit def toNullable1[T](a: Array[T]): Array[T|Null] = a.asInstanceOf[Array[T|Null]] implicit def toNullable2[T](a: Array[Array[T]]): Array[Array[T|Null]|Null] = a.asInstanceOf[Array[Array[T|Null]|Null]] + + implicit def fromNullable1[T](a: Array[T|Null]): Array[T] = a.asInstanceOf[Array[T]] + implicit def fromNullable2[T](a: Array[Array[T|Null]|Null]): Array[Array[T]] = a.asInstanceOf[Array[Array[T]]] } } From 64146a7f7968a3b2751ffa2a32453030fa734f3b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 14:34:17 -0500 Subject: [PATCH 104/127] Fix bugs introduced when adding support for repeated params Bugs introduced in https://github.com/abeln/dotty/commit/6e2462d4cfa8253f7439e9bf0af6fe6481db93eb This fixes tests/run/f-interpolation-1 --- compiler/src/dotty/tools/dotc/core/Types.scala | 9 +-------- .../src/dotty/tools/dotc/typer/Applications.scala | 2 +- tests/run/explicit-null-byname-varargs.scala | 11 +++++++++++ 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 tests/run/explicit-null-byname-varargs.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d61186e1c6d1..f740e584ce0a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1273,14 +1273,7 @@ object Types { /** If this is a repeated type, its element type, otherwise the type itself */ def repeatedToSingle(implicit ctx: Context): Type = this match { case tp @ ExprType(tp1) => tp.derivedExprType(tp1.repeatedToSingle) - case _ => - if (isRepeatedParam) { - val repTpe = stripJavaNull - assert(repTpe.argTypesLo.size == 1, s"Found repeated parameter type with more than one argument: ${this.show}") - repTpe.argTypesLo.head - } else { - this - } + case _ => if (isRepeatedParam) stripJavaNull.argTypesHi.head else this } /** If this is a FunProto or PolyProto, WildcardType, otherwise this. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 389cd07d6d22..844f9f149e42 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -531,7 +531,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => case arg :: Nil if isVarArg(arg) => addTyped(arg, formal) case _ => - val elemFormal = formal.repeatedToSingle + val elemFormal = formal.widenExpr.repeatedToSingle val typedArgs = harmonic(harmonizeArgs, elemFormal)(args.map(typedArg(_, elemFormal))) typedArgs.foreach(addArg(_, elemFormal)) diff --git a/tests/run/explicit-null-byname-varargs.scala b/tests/run/explicit-null-byname-varargs.scala new file mode 100644 index 000000000000..13023d0dd6f3 --- /dev/null +++ b/tests/run/explicit-null-byname-varargs.scala @@ -0,0 +1,11 @@ + +// Test that by-name varargs don't crash at runtime. +object Test { + def foo(x: => Any*): Unit = x + + def main(args: Array[String]): Unit = { + foo("hello") + foo(42) + foo("a", "b", 200) + } +} From 486dda4fd90c640dc578bddf8af4dfa9ef95384c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 14:58:41 -0500 Subject: [PATCH 105/127] fix test --- tests/run/t8177f.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/run/t8177f.scala b/tests/run/t8177f.scala index 6f9a68c11b75..42153e0cede3 100644 --- a/tests/run/t8177f.scala +++ b/tests/run/t8177f.scala @@ -7,7 +7,7 @@ class A[T](final val x: Thing { type A = T }) { def x3: x.A = x.p } // all result types should be inferred as Int -class B extends A[Int](null) { +class B extends A[Int](new Thing { type A = Int }) { def y1 = x1 def y2 = x2 val y3 = x3 // before SI-8177, this lead to a signature that erased to java.lang.Object @@ -15,6 +15,14 @@ class B extends A[Int](null) { object Test extends dotty.runtime.LegacyApp { - val methods = classOf[B].getDeclaredMethods.sortBy(_.getName) - assert(methods.forall(_.toGenericString.startsWith("public int"))) + val methods = classOf[B].getDeclaredMethods.sortBy(_.nn.getName.nn) + // B's methods are + // private static Thing B.B$superArg$1() + // public int B.y1() + // public int B.y2() + // public int B.y3() + assert(methods.forall { m => + val name = m.nn.toGenericString + name.startsWith("private") || name.startsWith("public int") + }) } From 039259fc5dd6364d6a766d0d454a08113b340519 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 15:16:13 -0500 Subject: [PATCH 106/127] fix test --- tests/run/t6260-delambdafy.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/t6260-delambdafy.scala b/tests/run/t6260-delambdafy.scala index a4a3cfc8ff02..14a47e2151a5 100644 --- a/tests/run/t6260-delambdafy.scala +++ b/tests/run/t6260-delambdafy.scala @@ -4,7 +4,7 @@ object Test { val f = (x: C[Any]) => {println(s"f($x)"); x} def main(args: Array[String]): Unit = { f(new C(".")) - val methods = f.getClass.getDeclaredMethods.map(_.getName).sorted + val methods = f.getClass.getDeclaredMethods.map(_.nn.getName.nn).sorted println("") println(methods.mkString("\n")) } From 6755bbdb70d90bd23dcf7bf2683174222672fe15 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 16:15:26 -0500 Subject: [PATCH 107/127] Fix bug when expanding SAM types We weren't handling implicit nullary functions. --- .../src/dotty/tools/dotc/transform/ExpandSAMs.scala | 10 ++++------ tests/pos/explicit-null-sam-types.scala | 6 ++++++ tests/run/sams.scala | 6 ++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index 572bad944a9e..c1584b9c81bf 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -19,6 +19,7 @@ import dotty.tools.dotc.util.Positions.Position * 4. Closures that implement traits which run initialization code. * 5. Closures that get synthesized abstract methods in the transformation pipeline. These methods can be * (1) superaccessors, (2) outer references, (3) accessors for fields. + * 6. Nullable functions: e.g. `(Int => Int)|Null` * * However, implicit function types do not count as SAM types. */ @@ -45,12 +46,9 @@ class ExpandSAMs extends MiniPhase { // In other words, `val x: Int => Int = (x) => x` and `val x: (Int => Int)|Null = (x) => x` both typecheck, // but the former uses the platform SAM types, vs the latter which uses an anonymous class. val tpe = tpt.tpe.stripNull - // TODO(abeln): remove comment below after review. - // The lines below were commented out because implicit function types should be handled - // in `case NoType` above, as per the comment on the contents of `tpe` in the definition of `Closure`. - // if (defn.isImplicitFunctionType(tpe)) { - // tree - if (tpe.isRef(defn.PartialFunctionClass)) { + if (defn.isImplicitFunctionType(tpe)) { + tree + } else if (tpe.isRef(defn.PartialFunctionClass)) { val tpe1 = checkRefinements(tpe, fn.pos) toPartialFunction(tree, tpe1) } else if (isPlatformSam(tpe.classSymbol.asClass)) { diff --git a/tests/pos/explicit-null-sam-types.scala b/tests/pos/explicit-null-sam-types.scala index 4cf020400747..64bc30c5e774 100644 --- a/tests/pos/explicit-null-sam-types.scala +++ b/tests/pos/explicit-null-sam-types.scala @@ -18,4 +18,10 @@ class Foo { def foo(m: Int) = {} foo((x) => "hello") + + // Implicit nullary function + { + val f: implicit () => String = "hello" + f + } } diff --git a/tests/run/sams.scala b/tests/run/sams.scala index bc076f4bcc87..d691ff4e5494 100644 --- a/tests/run/sams.scala +++ b/tests/run/sams.scala @@ -93,6 +93,12 @@ object Test { assert(rf(1, "hello") == 1) assert(sf("abc") == 1) + // Implicit nullary function + { + val f: implicit () => Int = 42 + assert (f == 42) + } + // With null { val f: (Int => Int)|Null = (x: Int) => 2 From 42b49a4a6b334bc3ac8b4e90b755a14b8f8f7b13 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 16:41:09 -0500 Subject: [PATCH 108/127] fix tests --- compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala | 2 +- tests/run/2772.scala | 2 +- tests/run/opaque-immutable-array-xm.scala | 4 ++-- tests/run/patmat-option-named.scala | 2 +- tests/run/t8197.scala | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index c1584b9c81bf..904667af8a84 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -36,7 +36,7 @@ class ExpandSAMs extends MiniPhase { case Block(stats @ (fn: DefDef) :: Nil, Closure(_, fnRef, tpt)) if fnRef.symbol == fn.symbol => tpt.tpe match { case NoType => - tree // it's a plain or implicit function + tree // it's a plain function case SAMType(_) => // If the type is a SAM type, it's also possibly nullable. However, here we're desugaring an // expression of the form `def fun() = {...}; closure(fun)`, so the desugaring should use the diff --git a/tests/run/2772.scala b/tests/run/2772.scala index fcde4c363b35..8268105977f1 100644 --- a/tests/run/2772.scala +++ b/tests/run/2772.scala @@ -2,7 +2,7 @@ import java.io.OutputStream object Test { def main(args: Array[String]): Unit = { - val oldErr = System.err + val oldErr = System.err.nn System.setErr(null) // setOut(null) confuses the testing framework... val a = () => foo(oldErr) a() diff --git a/tests/run/opaque-immutable-array-xm.scala b/tests/run/opaque-immutable-array-xm.scala index ffd07c59047e..896b5acd88d3 100644 --- a/tests/run/opaque-immutable-array-xm.scala +++ b/tests/run/opaque-immutable-array-xm.scala @@ -16,7 +16,7 @@ object Test extends App { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, (ia: Array[A]).length) + val arr = Arrays.copyOf(ia, (ia: Array[A]).length).nn scala.util.Sorting.quickSort(arr) arr } @@ -48,4 +48,4 @@ object Test extends App { val xs: IArray[Long] = IArray(1L, 2L, 3L) assert(binaryIndexOf(xs, 2L) == 1) assert(binaryIndexOf(xs, 4L) < 0) -} \ No newline at end of file +} diff --git a/tests/run/patmat-option-named.scala b/tests/run/patmat-option-named.scala index b27d07107709..0178a9a2da08 100644 --- a/tests/run/patmat-option-named.scala +++ b/tests/run/patmat-option-named.scala @@ -1,4 +1,4 @@ -case class HasSingleField(f: HasSingleField) +case class HasSingleField(f: HasSingleField|Null) object Test { diff --git a/tests/run/t8197.scala b/tests/run/t8197.scala index b510f96f1b38..ebcea88b0fa6 100644 --- a/tests/run/t8197.scala +++ b/tests/run/t8197.scala @@ -1,10 +1,10 @@ // SI-8197, see also SI-4592 and SI-4728 -class A +class A(val tag: String) class B -class Foo(val x: A = null) { +class Foo(val x: A = new A("default")) { def this(bla: B*) = { - this(new A) + this(new A("varargs")) } } @@ -12,5 +12,5 @@ object Test extends dotty.runtime.LegacyApp { // both constructors of `Foo` are applicable. Overloading resolution // will eliminate the alternative that uses a default argument, therefore // the vararg constructor is chosen. - assert((new Foo).x != null) + assert((new Foo).x.tag == "varargs") } From e4e66df26b47f6ec1a3fed3202bb8f2e02f7bc83 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 7 Jan 2019 16:47:15 -0500 Subject: [PATCH 109/127] fix tests --- tests/run/concat-two-strings.scala | 2 +- tests/run/t1718.scala | 4 ++-- tests/run/t8233.scala | 2 +- tests/run/unit-lazy-val.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/run/concat-two-strings.scala b/tests/run/concat-two-strings.scala index c8881aa14627..4731b3f736bd 100644 --- a/tests/run/concat-two-strings.scala +++ b/tests/run/concat-two-strings.scala @@ -4,7 +4,7 @@ object Test { def f1(x: AnyRef) = "" + x def f2(x: Int) = "" + x - def f3(x: Array[Char]) = "" + x + def f3(x: Array[Char]|Null) = "" + x def f4(x: List[Int]) = "" + x def f5(x: Any) = "" + x def f6(x: AnyVal) = "" + x diff --git a/tests/run/t1718.scala b/tests/run/t1718.scala index e6f52025448a..ee86122216e9 100644 --- a/tests/run/t1718.scala +++ b/tests/run/t1718.scala @@ -1,10 +1,10 @@ object Test extends dotty.runtime.LegacyApp{ - def matchesNull[T](mightBeNull: Array[T]): Boolean = mightBeNull match { + def matchesNull[T](mightBeNull: Array[T]|Null): Boolean = mightBeNull match { case null => true case x => false } - val nullArray: Array[String] = null + val nullArray: Array[String]|Null = null println(matchesNull(nullArray)) } diff --git a/tests/run/t8233.scala b/tests/run/t8233.scala index 3896a7cc6b12..c9455f7795b8 100644 --- a/tests/run/t8233.scala +++ b/tests/run/t8233.scala @@ -1,5 +1,5 @@ object Test { - def bar(s: String) = s; + def bar(s: String|Null) = s; val o: Option[Null] = None def nullReference: Unit = { val a: Null = o.get diff --git a/tests/run/unit-lazy-val.scala b/tests/run/unit-lazy-val.scala index a93d28c81ade..c5538538b7e6 100644 --- a/tests/run/unit-lazy-val.scala +++ b/tests/run/unit-lazy-val.scala @@ -10,7 +10,7 @@ object Test { // TODO: Erase // Currently not erasing fields for lazy vals - assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + assert(f.getClass.getDeclaredFields.exists(_.nn.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") } From f5fcaa0e1c869383a83fbcdf10f3cf08387b99d6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 8 Jan 2019 13:46:35 -0500 Subject: [PATCH 110/127] fix typo --- library/src-bootstrapped/scala/NonNull.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src-bootstrapped/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala index 8b73f59bc46b..7063830cf2fc 100644 --- a/library/src-bootstrapped/scala/NonNull.scala +++ b/library/src-bootstrapped/scala/NonNull.scala @@ -3,7 +3,7 @@ package scala object NonNull { implicit class NonNull[T](x: T|Null) { def nn: T = if (x == null) { - throw new NullPointerException("tried cast away nullability, but value is null") + throw new NullPointerException("tried to cast away nullability, but value is null") } else { x.asInstanceOf[T] } From 8bd78997894b59d44edcb33a552a4a18a1a51cf8 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 10 Jan 2019 19:21:32 -0500 Subject: [PATCH 111/127] Make Null a subtype of Any, instead of AnyRef Null <: Any makes for a cleaner type hierarchy. For example, we can now abstract over non-nullable types with `def foo[T <: AnyRef](x: T) = ...` However, both AnyRef and Null need to be comparable with reference equality, so we add a new trait RefEq ``` trait RefEq { def eq(that: RefEq): Boolean def ne(that: RefEq): Boolean } ``` and make both AnyRef and Null extend RefEq. RefEq is completely synthetic, and it gets erased to Object. --- .../tools/backend/jvm/scalaPrimitives.scala | 10 +++++-- .../dotty/tools/dotc/core/Definitions.scala | 26 ++++++++++++----- .../src/dotty/tools/dotc/core/JavaNull.scala | 6 ++-- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 7 +++++ .../dotty/tools/dotc/transform/Erasure.scala | 12 +++++--- .../src/dotty/tools/dotc/typer/Namer.scala | 4 ++- library/src-bootstrapped/scala/NonNull.scala | 2 +- tests/neg/explicit-null-override/J.java | 1 + tests/neg/explicit-null-override/S.scala | 13 +++++++++ tests/neg/explicit-null-subtype-any.scala | 17 +++++++++++ tests/pos/extractors.scala | 2 +- tests/pos/i2152.scala | 4 +-- tests/pos/opaque-nullable.scala | 8 +++--- tests/pos/t0154.scala | 2 +- tests/pos/t2619.scala | 4 +-- tests/pos/tailcall/i321.scala | 4 +-- tests/pos/tailcall/tailcall.scala | 2 +- tests/run/explicit-null-subtype-any.scala | 28 +++++++++++++++++++ tests/run/i3539.scala | 8 +++--- tests/run/matchnull.scala | 4 +-- tests/run/nullInstanceEval.scala | 2 +- tests/run/patmat-finally.scala | 2 +- tests/run/runtime.scala | 2 +- tests/run/t6168b/main.scala | 2 +- tests/run/t8601d.scala | 2 +- tests/run/try.scala | 2 +- 28 files changed, 135 insertions(+), 44 deletions(-) create mode 100644 tests/neg/explicit-null-subtype-any.scala create mode 100644 tests/run/explicit-null-subtype-any.scala diff --git a/compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala b/compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala index 66c247167ed8..ebfeeebe6eb1 100644 --- a/compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala +++ b/compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala @@ -155,11 +155,17 @@ class DottyPrimitives(ctx: Context) { addPrimitive(defn.Any_asInstanceOf, AS) addPrimitive(defn.Any_##, HASH) + // scala.Reference + addPrimitive(defn.RefEq_eq, ID) + addPrimitive(defn.RefEq_ne, NI) + // java.lang.Object + /* addPrimitive(defn.Object_eq, ID) addPrimitive(defn.Object_ne, NI) - /* addPrimitive(defn.Any_==, EQ) - addPrimitive(defn.Any_!=, NE)*/ + addPrimitive(defn.Any_==, EQ) + addPrimitive(defn.Any_!=, NE) + */ addPrimitive(defn.Object_synchronized, SYNCHRONIZED) /*addPrimitive(defn.Any_isInstanceOf, IS) addPrimitive(defn.Any_asInstanceOf, AS)*/ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 62417d209d7d..d571363c011a 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -278,7 +278,7 @@ class Definitions { lazy val ObjectClass: ClassSymbol = { val cls = ctx.requiredClass("java.lang.Object") assert(!cls.isCompleted, "race for completing java.lang.Object") - cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: Nil, newScope) + cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: RefEqClass.typeRef :: Nil, newScope) cls.setFlag(NoInits) // The companion object doesn't really exist, `NoType` is the general @@ -295,8 +295,9 @@ class Definitions { lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType) def AnyRefType: TypeRef = AnyRefAlias.typeRef - lazy val Object_eq: TermSymbol = enterMethod(ObjectClass, nme.eq, methOfAnyRef(BooleanType), Final) - lazy val Object_ne: TermSymbol = enterMethod(ObjectClass, nme.ne, methOfAnyRef(BooleanType), Final) + // TODO(abeln): modify usage sites to use `RefEq_eq/ne`? + lazy val Object_eq: TermSymbol = RefEq_eq + lazy val Object_ne: TermSymbol = RefEq_ne lazy val Object_synchronized: TermSymbol = enterPolyMethod(ObjectClass, nme.synchronized_, 1, pt => MethodType(List(pt.paramRefs(0)), pt.paramRefs(0)), Final) lazy val Object_clone: TermSymbol = enterMethod(ObjectClass, nme.clone_, MethodType(Nil, ObjectType), Protected) @@ -334,8 +335,18 @@ class Definitions { ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef)) def NothingType: TypeRef = NothingClass.typeRef lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing") + + lazy val RefEqClass: ClassSymbol = enterCompleteClassSymbol( + ScalaPackageClass, tpnme.RefEq, Trait, AnyClass.typeRef :: Nil) + def RefEqType: TypeRef = RefEqClass.typeRef + + lazy val RefEq_eq: TermSymbol = enterMethod(RefEqClass, nme.eq, MethodType(List(RefEqType), BooleanType), Final) + lazy val RefEq_ne: TermSymbol = enterMethod(RefEqClass, nme.ne, MethodType(List(RefEqType), BooleanType), Final) + + def RefEqMethods: List[TermSymbol] = List(RefEq_eq, RefEq_ne) + lazy val NullClass: ClassSymbol = enterCompleteClassSymbol( - ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef)) + ScalaPackageClass, tpnme.Null, AbstractFinal, AnyClass.typeRef :: RefEqClass.typeRef :: Nil) def NullType: TypeRef = NullClass.typeRef lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null") @@ -1085,7 +1096,7 @@ class Definitions { lazy val UnqualifiedOwnerTypes: Set[NamedType] = RootImportTypes.toSet[NamedType] ++ RootImportTypes.map(_.symbol.moduleClass.typeRef) - lazy val NotRuntimeClasses: Set[Symbol] = Set(AnyClass, AnyValClass, NullClass, NothingClass) + lazy val NotRuntimeClasses: Set[Symbol] = Set(AnyClass, AnyValClass, RefEqClass, NullClass, NothingClass) /** Classes that are known not to have an initializer irrespective of * whether NoInits is set. Note: FunctionXXLClass is in this set @@ -1273,13 +1284,14 @@ class Definitions { def isValueSubClass(sym1: Symbol, sym2: Symbol): Boolean = valueTypeEnc(sym2.asClass.name) % valueTypeEnc(sym1.asClass.name) == 0 - lazy val erasedToObject: Set[Symbol] = Set(AnyClass, AnyValClass, TupleClass, NonEmptyTupleClass, SingletonClass) + lazy val erasedToObject: Set[Symbol] = Set(AnyClass, AnyValClass, RefEqClass, TupleClass, NonEmptyTupleClass, SingletonClass) // ----- Initialization --------------------------------------------------- /** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */ lazy val syntheticScalaClasses: List[TypeSymbol] = List( AnyClass, + RefEqClass, AnyRefAlias, AnyKindClass, RepeatedParamClass, @@ -1296,7 +1308,7 @@ class Definitions { /** Lists core methods that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */ lazy val syntheticCoreMethods: List[TermSymbol] = - AnyMethods ++ ObjectMethods ++ List(String_+, throwMethod) + AnyMethods ++ ObjectMethods ++ RefEqMethods ++ List(String_+, throwMethod) lazy val reservedScalaClassNames: Set[Name] = syntheticScalaClasses.map(_.name).toSet diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index d8c1bf3bce4f..c97ebdcd9aec 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -142,9 +142,9 @@ object JavaNull { tp match { case tp: TypeRef => !tp.symbol.isValueClass && - !tp.symbol.derivesFrom(defn.AnnotationClass) && - !tp.isRef(defn.ObjectClass) && - !tp.isRef(defn.AnyClass) + !tp.isRef(defn.AnyClass) && + !tp.isRef(defn.RefEqClass) && + !tp.symbol.derivesFrom(defn.AnnotationClass) case _ => true } diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 047687a982d3..08ef353e0eab 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -160,6 +160,7 @@ object StdNames { scala.List(Byte, Char, Short, Int, Long, Float, Double, Boolean, Unit) // some types whose companions we utilize + final val RefEq: N = "RefEq" final val AnyRef: N = "AnyRef" final val Array: N = "Array" final val List: N = "List" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 08d020e688c5..b7753b854045 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -682,7 +682,7 @@ object SymDenotations { /** Is this symbol a class with nullable values? */ final def isNullableClass(implicit ctx: Context): Boolean = { - if (!ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyRefAlias || symbol == defn.AnyClass + if (!ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyClass else isNullableClassAfterErasure } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f740e584ce0a..f71868c297d5 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -459,6 +459,13 @@ object Types { val rsym = r.classSymbol if (lsym isSubClass rsym) rsym else if (rsym isSubClass lsym) lsym + else if (this.isNullableUnion) { + val OrType(left, _) = this.normalizeNull + // If `left` is a reference type, then the class LUB of `left | Null` is `Reference`. + // This is another one-of case that keeps this method sound, but not complete. + if (left.classSymbol isSubClass defn.ObjectClass) defn.RefEqClass + else NoSymbol + } else NoSymbol case _ => NoSymbol diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index bafea790faba..cf23df7f7895 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -54,8 +54,12 @@ class Erasure extends Phase with DenotTransformer { // After erasure, all former Any members are now Object members val ClassInfo(pre, _, ps, decls, selfInfo) = ref.info val extendedScope = decls.cloneScope - for (decl <- defn.AnyClass.classInfo.decls) - if (!decl.isConstructor) extendedScope.enter(decl) + def addDecls(cls: ClassSymbol) = { + for (decl <- cls.classInfo.decls) + if (!decl.isConstructor) extendedScope.enter(decl) + } + addDecls(defn.AnyClass) + addDecls(defn.RefEqClass) ref.copySymDenotation( info = transformInfo(ref.symbol, ClassInfo(pre, defn.ObjectClass, ps, extendedScope, selfInfo)) @@ -68,7 +72,7 @@ class Erasure extends Phase with DenotTransformer { defn.ObjectClass.primaryConstructor else oldSymbol val oldOwner = ref.owner - val newOwner = if (oldOwner eq defn.AnyClass) defn.ObjectClass else oldOwner + val newOwner = if ((oldOwner eq defn.AnyClass) || (oldOwner eq defn.RefEqClass)) defn.ObjectClass else oldOwner val oldInfo = ref.info val newInfo = transformInfo(oldSymbol, oldInfo) val oldFlags = ref.flags @@ -399,7 +403,7 @@ object Erasure { def mapOwner(sym: Symbol): Symbol = { def recur(owner: Symbol): Symbol = if (defn.erasedToObject.contains(owner)) { - assert(sym.isConstructor, s"${sym.showLocated}") +// assert(sym.isConstructor, s"${sym.showLocated}") defn.ObjectClass } else if (defn.isSyntheticFunctionClass(owner)) defn.erasedFunctionClass(owner) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 36b6fa339eb5..728e1a0e23d2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -137,9 +137,11 @@ trait NamerContextOps { this: Context => if (params.isEmpty) (false, false) else (params.head is Implicit, params.head is Erased) val make = MethodType.maker(isJava, isImplicit, isErased) + // Following Scalac, Namer needs to convert Java method parameters + // of type j.l.Object|JavaNull to s.Any. if (isJava) for (param <- params) - if (param.info.isDirectRef(defn.ObjectClass)) param.info = defn.AnyType + if (param.info.stripNull.isDirectRef(defn.ObjectClass)) param.info = defn.AnyType make.fromSymbols(params, resultType) } if (typeParams.nonEmpty) PolyType.fromParams(typeParams.asInstanceOf[List[TypeSymbol]], monotpe) diff --git a/library/src-bootstrapped/scala/NonNull.scala b/library/src-bootstrapped/scala/NonNull.scala index 7063830cf2fc..11c80789f046 100644 --- a/library/src-bootstrapped/scala/NonNull.scala +++ b/library/src-bootstrapped/scala/NonNull.scala @@ -1,7 +1,7 @@ package scala object NonNull { - implicit class NonNull[T](x: T|Null) { + implicit class NonNull[T](x: T|Null) extends AnyVal { def nn: T = if (x == null) { throw new NullPointerException("tried to cast away nullability, but value is null") } else { diff --git a/tests/neg/explicit-null-override/J.java b/tests/neg/explicit-null-override/J.java index 6ca85c44b958..e7bead32059a 100644 --- a/tests/neg/explicit-null-override/J.java +++ b/tests/neg/explicit-null-override/J.java @@ -3,4 +3,5 @@ class C {} class J { void foo(String x){ } void bar(C>> x){} + Object zoom(Object x){ return x; } } diff --git a/tests/neg/explicit-null-override/S.scala b/tests/neg/explicit-null-override/S.scala index 5f2371ee0f7b..b2ca1eec9caf 100644 --- a/tests/neg/explicit-null-override/S.scala +++ b/tests/neg/explicit-null-override/S.scala @@ -14,6 +14,18 @@ class S3 extends J { override def bar(x: C[C[C[String|Null]]]): Unit = {} // error: since the null transform doesn't add nulls in the inside, neither should the Scala user } +class S4 extends J { + override def zoom(x: Object): Object = x +} + +class S5 extends J { + override def zoom(x: Object): Object|Null = x +} + +class S6 extends J { + override def zoom(x: Object|Null): Object|Null = x +} + class Base { def foo(x: String): Unit = {} def bar(x: String|Null): Unit = {} @@ -23,3 +35,4 @@ class Derived extends Base { override def foo(x: String|Null): Unit = {} // error: can't ignore null when extending from Scala override def bar(x: String): Unit = {} // error: can't ignore null when extending from Scala } + diff --git a/tests/neg/explicit-null-subtype-any.scala b/tests/neg/explicit-null-subtype-any.scala new file mode 100644 index 000000000000..eef5c7296c35 --- /dev/null +++ b/tests/neg/explicit-null-subtype-any.scala @@ -0,0 +1,17 @@ + +// Check that Null is a subtype of Any, but not of AnyRef +class Foo { + + val x1: Any = null + val x2: AnyRef = null // error + val x3: AnyRef|Null = null + val x4: Any|Null = null // Any|Null == Any + + { + def bar(a: Any): Unit = () + val s: String|Null = ??? + bar(s) + val s2: Int|Null = ??? + bar(s2) + } +} diff --git a/tests/pos/extractors.scala b/tests/pos/extractors.scala index 495dfdafda71..ca6d4c6d648b 100644 --- a/tests/pos/extractors.scala +++ b/tests/pos/extractors.scala @@ -3,7 +3,7 @@ object test { class Tree class Apply(val fun: Tree, val args: List[Tree]) extends Tree - trait DeconstructorCommon[T >: Null <: AnyRef] { + trait DeconstructorCommon[T >: Null <: AnyRef|Null] { var field: T = null def get: this.type = this def isEmpty: Boolean = field eq null diff --git a/tests/pos/i2152.scala b/tests/pos/i2152.scala index 2171a487e919..62e2c249672f 100644 --- a/tests/pos/i2152.scala +++ b/tests/pos/i2152.scala @@ -1,6 +1,6 @@ -class Contra[-D](task: AnyRef) +class Contra[-D](task: AnyRef|Null) object Test { - def narrow(task: AnyRef): Contra[task.type] = new Contra(task) + def narrow(task: AnyRef|Null): Contra[task.type] = new Contra(task) def ident[Before](elems: Contra[Before]): Contra[Before] = elems val foo = null ident(narrow(foo)) diff --git a/tests/pos/opaque-nullable.scala b/tests/pos/opaque-nullable.scala index ce32d7c1cfed..a6954d3c71da 100644 --- a/tests/pos/opaque-nullable.scala +++ b/tests/pos/opaque-nullable.scala @@ -1,10 +1,10 @@ object nullable { - opaque type Nullable[A >: Null <: AnyRef] = A + opaque type Nullable[A >: Null <: AnyRef|Null] = A object Nullable { - def apply[A >: Null <: AnyRef](a: A): Nullable[A] = a + def apply[A >: Null <: AnyRef|Null](a: A): Nullable[A] = a - implicit class NullableOps[A >: Null <: AnyRef](na: Nullable[A]) { + implicit class NullableOps[A >: Null <: AnyRef|Null](na: Nullable[A]) { def exists(p: A => Boolean): Boolean = na != null && p(na) def filter(p: A => Boolean): Nullable[A] = @@ -23,4 +23,4 @@ object nullable { Option(na) } } -} \ No newline at end of file +} diff --git a/tests/pos/t0154.scala b/tests/pos/t0154.scala index 9fb9430676cb..c2bd1eaf046b 100644 --- a/tests/pos/t0154.scala +++ b/tests/pos/t0154.scala @@ -2,7 +2,7 @@ package test trait MyMatchers { val StringMatch = new AnyRef {} trait Something { - (null : AnyRef) match { + (null : AnyRef|Null) match { case (StringMatch) => case _ => } diff --git a/tests/pos/t2619.scala b/tests/pos/t2619.scala index 283d93bf2b97..e6c028ae85c6 100644 --- a/tests/pos/t2619.scala +++ b/tests/pos/t2619.scala @@ -1,11 +1,11 @@ abstract class F { - final def apply(x: Int): AnyRef = null + final def apply(x: Int): AnyRef|Null = null } abstract class AbstractModule { def as: List[AnyRef] def ms: List[AbstractModule] def fs: List[F] = Nil - def rs(x: Int): List[AnyRef] = fs.map(_(x)) + def rs(x: Int): List[AnyRef|Null] = fs.map(_(x)) } abstract class ModuleType1 extends AbstractModule {} abstract class ModuleType2 extends AbstractModule {} diff --git a/tests/pos/tailcall/i321.scala b/tests/pos/tailcall/i321.scala index daa078dd5093..d22f437e3ed8 100644 --- a/tests/pos/tailcall/i321.scala +++ b/tests/pos/tailcall/i321.scala @@ -11,7 +11,7 @@ import scala.annotation.tailrec * For now decision is such - we will abstract for top-level methods, but will not for inner ones. */ -class i321[T >: Null <: AnyRef] { +class i321[T >: Null <: AnyRef|Null] { def go1(f: T => Int): Int = { @tailrec def loop(pending: T): Int = { @@ -21,6 +21,6 @@ class i321[T >: Null <: AnyRef] { loop(null) } - final def go2[U >: Null <: AnyRef](t: i321[U]): Int = t.go2(this) + final def go2[U >: Null <: AnyRef|Null](t: i321[U]): Int = t.go2(this) } diff --git a/tests/pos/tailcall/tailcall.scala b/tests/pos/tailcall/tailcall.scala index faa707e18186..4d790c1a2173 100644 --- a/tests/pos/tailcall/tailcall.scala +++ b/tests/pos/tailcall/tailcall.scala @@ -1,7 +1,7 @@ class tailcall { val shift = 1 final def fact(x: Int, acc: Int = 1): Int = if (x == 0) acc else fact(x - shift, acc * x) - def id[T <: AnyRef](x: T): T = if (x eq null) x else id(x) + def id[T <: AnyRef|Null](x: T): T = if (x eq null) x else id(x) } class TypedApply[T2]{ diff --git a/tests/run/explicit-null-subtype-any.scala b/tests/run/explicit-null-subtype-any.scala new file mode 100644 index 000000000000..31a5bb66f092 --- /dev/null +++ b/tests/run/explicit-null-subtype-any.scala @@ -0,0 +1,28 @@ + +object Test { + + def main(args: Array[String]): Unit = { + assert(null.eq(null)) + assert(!null.ne(null)) + + assert(!null.eq("hello")) + assert(null.ne("hello")) + + assert(!null.eq(4)) + assert(null.ne(4)) + + assert(!"hello".eq(null)) + assert("hello".ne(null)) + + assert(!4.eq(null)) + assert(4.ne(null)) + + val x: String|Null = null + assert(x.eq(null)) + assert(!x.ne(null)) + + val x2: AnyRef|Null = "world" + assert(!x2.eq(null)) + assert(x2.ne(null)) + } +} diff --git a/tests/run/i3539.scala b/tests/run/i3539.scala index ddef1650ceea..80798a78b6b3 100644 --- a/tests/run/i3539.scala +++ b/tests/run/i3539.scala @@ -1,16 +1,16 @@ object Test { def main(args: Array[String]): Unit = { val i2s = (x: Int) => "" - assert(i2s.asInstanceOf.asInstanceOf[AnyRef => String].apply(null) == "") + assert(i2s.asInstanceOf.asInstanceOf[AnyRef|Null => String].apply(null) == "") val i2i = (x: Int) => x + 1 - assert(i2i.asInstanceOf[AnyRef => Int].apply(null) == 1) + assert(i2i.asInstanceOf[AnyRef|Null => Int].apply(null) == 1) } } class Test { - asInstanceOf[Nothing].asInstanceOf[AnyRef => String] + asInstanceOf[Nothing].asInstanceOf[AnyRef|Null => String] - asInstanceOf[Nothing].asInstanceOf[AnyRef => String].apply(null) + asInstanceOf[Nothing].asInstanceOf[AnyRef|Null => String].apply(null) } diff --git a/tests/run/matchnull.scala b/tests/run/matchnull.scala index 2cc8550d4743..063e4e0b6e68 100644 --- a/tests/run/matchnull.scala +++ b/tests/run/matchnull.scala @@ -1,8 +1,8 @@ object Test { - def f1 = null match { case x: AnyRef => 1 case _ => -1 } + def f1 = (null: AnyRef|Null) match { case x: AnyRef => 1 case _ => -1 } def f2(x: Any) = x match { case 52 => 1 ; case null => -1 ; case _ => 0 } - def f3(x: AnyRef) = x match { case x: String => 1 ; case List(_) => 0 ; case null => -1 ; case _ => -2 } + def f3(x: AnyRef|Null) = x match { case x: String => 1 ; case List(_) => 0 ; case null => -1 ; case _ => -2 } def main(args: Array[String]): Unit = { println(f1) diff --git a/tests/run/nullInstanceEval.scala b/tests/run/nullInstanceEval.scala index 47b7153eccf4..2c4ffbf0521e 100644 --- a/tests/run/nullInstanceEval.scala +++ b/tests/run/nullInstanceEval.scala @@ -1,5 +1,5 @@ object Test extends App { val x = null assert(!x.isInstanceOf[String]) - assert(!(x: AnyRef).isInstanceOf[String]) + assert(!(x: Any).isInstanceOf[String]) } diff --git a/tests/run/patmat-finally.scala b/tests/run/patmat-finally.scala index 895fb79f79de..5e66cf0077ff 100644 --- a/tests/run/patmat-finally.scala +++ b/tests/run/patmat-finally.scala @@ -1,6 +1,6 @@ /** Test pattern matching and finally, see SI-5929. */ object Test extends dotty.runtime.LegacyApp { - def bar(s1: Object, s2: Object): Unit = { + def bar(s1: Object|Null, s2: Object|Null): Unit = { s1 match { case _ => } diff --git a/tests/run/runtime.scala b/tests/run/runtime.scala index 89348b294db8..e81498917bd3 100644 --- a/tests/run/runtime.scala +++ b/tests/run/runtime.scala @@ -134,7 +134,7 @@ object Test3Test { + "found: " + actual1 + " - " + actual1); def test(args: Array[String]): Unit = { - val foo1: AnyRef = null; + val foo1: AnyRef|Null = null; val foo2: AnyRef = new Foo(); val foo3: AnyRef = new Foo(); diff --git a/tests/run/t6168b/main.scala b/tests/run/t6168b/main.scala index b8871ba1a83d..e6723c13debf 100644 --- a/tests/run/t6168b/main.scala +++ b/tests/run/t6168b/main.scala @@ -4,5 +4,5 @@ object Test extends dotty.runtime.LegacyApp { JavaTest.main(null) var a1 : SomeClass = new SomeClass - var b1 : Object = a1.f.set(23) + var b1 : Object|Null = a1.f.set(23) } diff --git a/tests/run/t8601d.scala b/tests/run/t8601d.scala index a6962847cb83..7db0e90bd2b4 100644 --- a/tests/run/t8601d.scala +++ b/tests/run/t8601d.scala @@ -3,6 +3,6 @@ object Test { def check(x: => Any) = try { x; sys.error("failed to throw NPE") } catch { case _: NullPointerException => } def main(args: Array[String]): Unit = { - check(monitor(null)) + check(monitor(null.asInstanceOf[AnyRef])) } } diff --git a/tests/run/try.scala b/tests/run/try.scala index 2875ce9cf6c5..bd3f81eb7ac8 100644 --- a/tests/run/try.scala +++ b/tests/run/try.scala @@ -38,7 +38,7 @@ object Test extends AnyRef with App { Console.println(x + n); } - var instance: AnyRef = null; + var instance: AnyRef|Null = null; def try4 = { if (instance == null) { From 67c72ff7866a7fec632e2705af694b559fc684f6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 14 Jan 2019 15:25:31 -0500 Subject: [PATCH 112/127] Revert changes to overriding Go back to _not_ ignoring nullability during override checks. The original motivation was twofold: 1) make migration more easy (less type errors to fix in code that overrides java classes) 2) enable binary compatibility with pre and post nullability versions of a Scala library However, since our current approach to binary compatibility is to not do anything, point 2 is now moot. If and when we need to re-enable this in the future we can always do so. Until then, this eliminates a source of unsoundness. --- .../src/dotty/tools/dotc/core/JavaNull.scala | 4 +- .../src/dotty/tools/dotc/core/Types.scala | 16 ++------ tests/neg/explicit-null-override/J.java | 7 ---- tests/neg/explicit-null-override/S.scala | 38 ------------------- .../naming-resolution/callsite.scala | 2 +- tests/pos/i1747.scala | 2 +- tests/pos/i3273/Test_2.scala | 2 +- tests/pos/t229.scala | 2 +- tests/pos/t2484.scala | 4 +- tests/pos/t3622/Test.scala | 2 +- tests/pos/t4737/S_2.scala | 2 +- tests/pos/t5703/Impl.scala | 2 +- 12 files changed, 15 insertions(+), 68 deletions(-) delete mode 100644 tests/neg/explicit-null-override/J.java delete mode 100644 tests/neg/explicit-null-override/S.scala diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index c97ebdcd9aec..2043a9047ebf 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -4,7 +4,7 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags.JavaDefined import dotty.tools.dotc.core.StdNames.{jnme, nme} import dotty.tools.dotc.core.Symbols.{Symbol, defn, _} -import dotty.tools.dotc.core.Types.{AppliedType, LambdaType, MethodType, OrType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef} +import dotty.tools.dotc.core.Types.{AndType, AppliedType, LambdaType, MethodType, OrType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef} /** Transformation from Java (nullable) to Scala (non-nullable) types */ object JavaNull { @@ -167,6 +167,8 @@ object JavaNull { mapOver(tp) case tp: TypeAlias => mapOver(tp) + case tp: AndType => + mapOver(tp) case tp: TypeRef if shouldNullify(tp) => op(tp) case tp: TypeParamRef => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f71868c297d5..e492816e5837 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -938,24 +938,14 @@ object Types { ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) } - /** This is the same as `matches` except that - * (1) it also matches => T with T and vice versa - * (2) it ignores "| JavaNull" unions embedded in the types + /** This is the same as `matches` except that it also matches => T with T and + * vice versa. */ def matchesLoosely(that: Type)(implicit ctx: Context): Boolean = (this matches that) || { var thisResult = this.widenExpr var thatResult = that.widenExpr - // If either of the types contains a `| JavaNull` union, then we want to get right of _all_ nullable - // unions that appear in places where Java code could have a `JavaNull`. - // e.g. we want `String|Null => String` to `String|JavaNull => String|JavaNull`. - if (JavaNull.containsJavaNullableUnions(thisResult) || JavaNull.containsJavaNullableUnions(thatResult)) { - thisResult = JavaNull.stripNullableUnions(thisResult) - thatResult = JavaNull.stripNullableUnions(thatResult) - } - // TODO(abeln): was the requirement that only of the types changes just an optimization? - // (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) - ((this ne thisResult) || (that ne thatResult)) && thisResult.matchesLoosely(thatResult) + (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ diff --git a/tests/neg/explicit-null-override/J.java b/tests/neg/explicit-null-override/J.java deleted file mode 100644 index e7bead32059a..000000000000 --- a/tests/neg/explicit-null-override/J.java +++ /dev/null @@ -1,7 +0,0 @@ -class C {} - -class J { - void foo(String x){ } - void bar(C>> x){} - Object zoom(Object x){ return x; } -} diff --git a/tests/neg/explicit-null-override/S.scala b/tests/neg/explicit-null-override/S.scala deleted file mode 100644 index b2ca1eec9caf..000000000000 --- a/tests/neg/explicit-null-override/S.scala +++ /dev/null @@ -1,38 +0,0 @@ -// Check that `|JavaNull` is ignored in override checks - -class S extends J { - override def foo(x: String): Unit = {} - override def bar(x: C[C[C[String]]]): Unit = {} -} - -class S2 extends J { - override def foo(x: String|Null): Unit = {} - override def bar(x: C[C[C[String]]]|Null): Unit = {} -} - -class S3 extends J { - override def bar(x: C[C[C[String|Null]]]): Unit = {} // error: since the null transform doesn't add nulls in the inside, neither should the Scala user -} - -class S4 extends J { - override def zoom(x: Object): Object = x -} - -class S5 extends J { - override def zoom(x: Object): Object|Null = x -} - -class S6 extends J { - override def zoom(x: Object|Null): Object|Null = x -} - -class Base { - def foo(x: String): Unit = {} - def bar(x: String|Null): Unit = {} -} - -class Derived extends Base { - override def foo(x: String|Null): Unit = {} // error: can't ignore null when extending from Scala - override def bar(x: String): Unit = {} // error: can't ignore null when extending from Scala -} - diff --git a/tests/pos-scala2/naming-resolution/callsite.scala b/tests/pos-scala2/naming-resolution/callsite.scala index 036803a26930..b36580702267 100644 --- a/tests/pos-scala2/naming-resolution/callsite.scala +++ b/tests/pos-scala2/naming-resolution/callsite.scala @@ -6,5 +6,5 @@ package naming.resolution import java.nio.file._ // Imports `Files` object Resolution { - def gimmeFiles: Files = Files.list(Paths.get(".")) + def gimmeFiles: Files|Null = Files.list(Paths.get(".")) } diff --git a/tests/pos/i1747.scala b/tests/pos/i1747.scala index 9be62a10a084..6e311898092d 100644 --- a/tests/pos/i1747.scala +++ b/tests/pos/i1747.scala @@ -1,3 +1,3 @@ abstract class Coll[E] extends java.util.Collection[E] { - override def toArray[T](a: Array[T with Object]): Array[T with Object] = ??? + override def toArray[T](a: Array[T with Object|Null]|Null): Array[T with Object|Null]|Null = ??? } diff --git a/tests/pos/i3273/Test_2.scala b/tests/pos/i3273/Test_2.scala index adc5e362493c..ea1fa844b04a 100644 --- a/tests/pos/i3273/Test_2.scala +++ b/tests/pos/i3273/Test_2.scala @@ -1,3 +1,3 @@ class Test extends Foo_1 { - override def foo(list: java.util.List[_]): Unit = ??? + override def foo(list: java.util.List[_]|Null): Unit = ??? } diff --git a/tests/pos/t229.scala b/tests/pos/t229.scala index 72ddfa74fec9..6a3f92ecd415 100644 --- a/tests/pos/t229.scala +++ b/tests/pos/t229.scala @@ -1,3 +1,3 @@ class Test extends java.util.ArrayList[Object] { - override def add(index: Int, element: java.lang.Object): Unit = {} + override def add(index: Int, element: java.lang.Object|Null): Unit = {} } diff --git a/tests/pos/t2484.scala b/tests/pos/t2484.scala index b822415fd262..1491dc999272 100644 --- a/tests/pos/t2484.scala +++ b/tests/pos/t2484.scala @@ -8,7 +8,7 @@ class Admin extends javax.swing.JApplet { //scala.concurrent.ops.spawn {someFunction ()} jScrollPane.addComponentListener { class nested extends java.awt.event.ComponentAdapter { - override def componentShown (e: java.awt.event.ComponentEvent) = { + override def componentShown (e: java.awt.event.ComponentEvent|Null) = { someFunction (); jScrollPane.removeComponentListener (this) } @@ -26,7 +26,7 @@ class Admin2 extends javax.swing.JApplet { scala.concurrent.Future {jScrollPane.synchronized { def someFunction () = {} //scala.concurrent.ops.spawn {someFunction ()} - jScrollPane.addComponentListener (new java.awt.event.ComponentAdapter {override def componentShown (e: java.awt.event.ComponentEvent) = { + jScrollPane.addComponentListener (new java.awt.event.ComponentAdapter {override def componentShown (e: java.awt.event.ComponentEvent|Null) = { someFunction (); jScrollPane.removeComponentListener (this)}}) }} } diff --git a/tests/pos/t3622/Test.scala b/tests/pos/t3622/Test.scala index d18953bbaca4..077dfb7318ab 100644 --- a/tests/pos/t3622/Test.scala +++ b/tests/pos/t3622/Test.scala @@ -1,5 +1,5 @@ package test class Test extends MyAsyncTask { - protected[test] def doInBackground1(args: Array[String]): String = "" + protected[test] def doInBackground1(args: Array[String|Null]|Null): String = "" } diff --git a/tests/pos/t4737/S_2.scala b/tests/pos/t4737/S_2.scala index dc89d13168c8..e9d889732daf 100644 --- a/tests/pos/t4737/S_2.scala +++ b/tests/pos/t4737/S_2.scala @@ -3,7 +3,7 @@ package s import j.J_1 class ScalaSubClass extends J_1 { - override def method(javaInnerClass: J_1#JavaInnerClass): Unit = { + override def method(javaInnerClass: J_1#JavaInnerClass|Null): Unit = { println("world") } } diff --git a/tests/pos/t5703/Impl.scala b/tests/pos/t5703/Impl.scala index f0120ef0b9a9..405400d01ac4 100644 --- a/tests/pos/t5703/Impl.scala +++ b/tests/pos/t5703/Impl.scala @@ -1,3 +1,3 @@ class Implementation extends Base[Object] { - def func(params: Array[Object]): Unit = {} + def func(params: Array[Object|Null]|Null): Unit = {} } From 253c36ac9001e0f261d80fc8eab8d9f88c0d9dfa Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 14 Jan 2019 18:44:21 -0500 Subject: [PATCH 113/127] Handle intersections in Java null transform In some cases during the null transform we see Java types of the form `A & B`, which weren't previously handled. Handle intersections by nf(A & B) = nf(A) & nf(B) | JavaNull (& binds stronger) but take care not to add JavaNull again while nullifying A and B. --- .../src/dotty/tools/dotc/core/JavaNull.scala | 95 +++++++------------ tests/neg/opaque-immutable-array.scala | 6 +- tests/run/opaque-immutable-array-xm.scala | 7 +- 3 files changed, 42 insertions(+), 66 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index 2043a9047ebf..7b96510d4c09 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -58,36 +58,6 @@ object JavaNull { } } - /** Adds "| JavaNull" to the relevant places of a Java type to reflect the fact - * that Java types remain nullable by default. - */ - def nullifyType(tpe: Type)(implicit ctx: Context): Type = { - val loc = new JavaNullLoc({ - case tp: OrType => tp - case tp => tp.toJavaNullable - }) - loc(tpe) - } - - /** Strip any `| Null` from locations where this transform would've inserted them. If `tpe` does not - * contain any nullable unions, then the same reference will be returned. - */ - def stripNullableUnions(tpe: Type)(implicit ctx: Context): Type = { - val loc = new JavaNullLoc(_.stripNull) - loc(tpe) - } - - /** Does `tpe` contain any `| JavaNull` in locations where the transform would've inserted them? */ - def containsJavaNullableUnions(tpe: Type)(implicit ctx: Context): Boolean = { - var hasUnion = false - val loc = new JavaNullLoc(tp => { - if (tp.isJavaNullableUnion) hasUnion = true - tp - }) - loc(tpe) - hasUnion - } - /** A policy that special cases the handling of some symbol or class of symbols. */ private sealed trait NullifyPolicy { /** Whether the policy applies to `sym`. */ @@ -136,25 +106,33 @@ object JavaNull { override def apply(tp: Type): Type = inner(tp) } - /** A type map that applies `op` just to the places where "|JavaNull" could potentially be added. */ - private class JavaNullLoc(op: Type => Type)(implicit ctx: Context) extends TypeMap { + /** Nullifies a Java type by adding `| JavaNull` in the relevant places. + * We need this because Java types remain implicitly nullable. + */ + private def nullifyType(tpe: Type)(implicit ctx: Context): Type = { + val nullMap = new JavaNullMap(alreadyNullable = false) + nullMap(tpe) + } + + /** A type map that adds `| JavaNull`. + * @param alreadyNullable whether the type being mapped is already nullable (at the outermost level) + */ + class JavaNullMap(alreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap { + /** Should we nullify `tp` at the outermost level? */ def shouldNullify(tp: Type): Boolean = { - tp match { - case tp: TypeRef => - !tp.symbol.isValueClass && - !tp.isRef(defn.AnyClass) && - !tp.isRef(defn.RefEqClass) && - !tp.symbol.derivesFrom(defn.AnnotationClass) - case _ => - true - } + !alreadyNullable && (tp match { + case tp: TypeRef => !tp.symbol.isValueClass && !tp.isRef(defn.AnyClass) && !tp.isRef(defn.RefEqClass) + case _ => true + }) } + /** Should we nullify the arguments to the given generic `tp`? + * We only nullify the inside of Scala-defined constructors. + * This is because Java classes are _all_ nullified, so both `java.util.List[String]` and + * `java.util.List[String|Null]` contain nullable elements. + */ def shouldDescend(tp: AppliedType): Boolean = { val AppliedType(tycons, _) = tp - // Only nullify the inside of Scala-defined constructors. - // This is because Java classes are _all_ nullified, so both `java.util.List[String]` and - // `java.util.List[String|Null]` contain nullable elements. tycons.widenDealias match { case tp: TypeRef if !tp.symbol.is(JavaDefined) => true case _ => false @@ -163,23 +141,20 @@ object JavaNull { override def apply(tp: Type): Type = { tp match { - case tp: LambdaType => - mapOver(tp) - case tp: TypeAlias => - mapOver(tp) - case tp: AndType => - mapOver(tp) - case tp: TypeRef if shouldNullify(tp) => - op(tp) - case tp: TypeParamRef => - op(tp) - case tp: OrType => - op(tp) - case appTp@AppliedType(tycons, targs) if shouldNullify(tp) => + case tp: LambdaType => mapOver(tp) + case tp: TypeAlias => mapOver(tp) + case tp@AndType(tp1, tp2) => + // nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add + // duplicate `Null`s at the outermost level inside `A` and `B`. + val newMap = new JavaNullMap(alreadyNullable = true) + derivedAndType(tp, newMap(tp1), newMap(tp2)).toJavaNullable + case tp: TypeRef if shouldNullify(tp) => tp.toJavaNullable + case tp: TypeParamRef if shouldNullify(tp) => tp.toJavaNullable + case tp: OrType => tp.toJavaNullable + case appTp@AppliedType(tycons, targs) => val targs2 = if (shouldDescend(appTp)) targs map this else targs - op(derivedAppliedType(appTp, tycons, targs2)) - case _ => - tp + derivedAppliedType(appTp, tycons, targs2).toJavaNullable + case _ => tp } } } diff --git a/tests/neg/opaque-immutable-array.scala b/tests/neg/opaque-immutable-array.scala index 995b0a1c9b56..131a56b333de 100644 --- a/tests/neg/opaque-immutable-array.scala +++ b/tests/neg/opaque-immutable-array.scala @@ -11,9 +11,9 @@ object ia { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, ia.length).nn - scala.util.Sorting.quickSort(arr) - arr + val arr = Arrays.copyOf(scala.NonNull.ArrayConversions.toNullable1(ia), ia.length).nn + scala.util.Sorting.quickSort(scala.NonNull.ArrayConversions.fromNullable1(arr)) + scala.NonNull.ArrayConversions.fromNullable1(arr) } // use a standard java method to search a sorted IArray. diff --git a/tests/run/opaque-immutable-array-xm.scala b/tests/run/opaque-immutable-array-xm.scala index 896b5acd88d3..7c888c87e99a 100644 --- a/tests/run/opaque-immutable-array-xm.scala +++ b/tests/run/opaque-immutable-array-xm.scala @@ -2,6 +2,7 @@ import scala.reflect.ClassTag object Test extends App { import java.util.Arrays + import scala.NonNull.ArrayConversions._ opaque type IArray[A1] = Array[A1] @@ -15,9 +16,9 @@ object Test extends App { def (ia: IArray[A]) apply[A] (i: Int): A = (ia: Array[A])(i) // return a sorted copy of the array - def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, (ia: Array[A]).length).nn - scala.util.Sorting.quickSort(arr) + def sorted[A <: AnyRef : math.Ordering](ia: IArray[A|Null]): IArray[A|Null] = { + val arr = Arrays.copyOf(ia, ia.length).nn + scala.util.Sorting.quickSort(scala.NonNull.ArrayConversions.fromNullable1(arr)) arr } From 246f70b852fc860165cdbcf602b26b5c9e0d185b Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 14 Jan 2019 18:56:40 -0500 Subject: [PATCH 114/127] Fix tests --- tests/pos/opaque-immutable-array.scala | 3 ++- tests/pos/sort.scala | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/pos/opaque-immutable-array.scala b/tests/pos/opaque-immutable-array.scala index 95a533127143..33feb20f1c24 100644 --- a/tests/pos/opaque-immutable-array.scala +++ b/tests/pos/opaque-immutable-array.scala @@ -13,7 +13,8 @@ object ia { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(ia, ia.length).nn + import scala.NonNull.ArrayConversions._ + val arr: Array[A] = Arrays.copyOf(ia, ia.length).nn scala.util.Sorting.quickSort(arr) arr } diff --git a/tests/pos/sort.scala b/tests/pos/sort.scala index 97ee3454d660..e7dce45a2a8c 100644 --- a/tests/pos/sort.scala +++ b/tests/pos/sort.scala @@ -1,5 +1,7 @@ object sorting { + import scala.NonNull.ArrayConversions._ + val xs: Array[String] = ??? java.util.Arrays.sort(xs, ???) From 320e38a9203eb540258f96f64d3abcb0712118cb Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Mon, 14 Jan 2019 19:00:03 -0500 Subject: [PATCH 115/127] Fix array tests to use implicit conversions --- tests/neg/opaque-immutable-array.scala | 7 ++++--- tests/run/opaque-immutable-array-xm.scala | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/neg/opaque-immutable-array.scala b/tests/neg/opaque-immutable-array.scala index 131a56b333de..ffb1a4ac3fa2 100644 --- a/tests/neg/opaque-immutable-array.scala +++ b/tests/neg/opaque-immutable-array.scala @@ -11,9 +11,10 @@ object ia { // return a sorted copy of the array def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { - val arr = Arrays.copyOf(scala.NonNull.ArrayConversions.toNullable1(ia), ia.length).nn - scala.util.Sorting.quickSort(scala.NonNull.ArrayConversions.fromNullable1(arr)) - scala.NonNull.ArrayConversions.fromNullable1(arr) + import scala.NonNull.ArrayConversions._ + val arr: Array[A] = Arrays.copyOf(ia, ia.length).nn + scala.util.Sorting.quickSort(arr) + arr } // use a standard java method to search a sorted IArray. diff --git a/tests/run/opaque-immutable-array-xm.scala b/tests/run/opaque-immutable-array-xm.scala index 7c888c87e99a..eb6fd731306c 100644 --- a/tests/run/opaque-immutable-array-xm.scala +++ b/tests/run/opaque-immutable-array-xm.scala @@ -16,9 +16,9 @@ object Test extends App { def (ia: IArray[A]) apply[A] (i: Int): A = (ia: Array[A])(i) // return a sorted copy of the array - def sorted[A <: AnyRef : math.Ordering](ia: IArray[A|Null]): IArray[A|Null] = { - val arr = Arrays.copyOf(ia, ia.length).nn - scala.util.Sorting.quickSort(scala.NonNull.ArrayConversions.fromNullable1(arr)) + def sorted[A <: AnyRef : math.Ordering](ia: IArray[A]): IArray[A] = { + val arr: Array[A] = Arrays.copyOf(ia, ia.length).nn + scala.util.Sorting.quickSort(arr) arr } From 5b012055345b367469e950d9936491145d4dcdc4 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 10:10:08 -0500 Subject: [PATCH 116/127] More robust handling of union types in JavaNull transform --- compiler/src/dotty/tools/dotc/core/JavaNull.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNull.scala b/compiler/src/dotty/tools/dotc/core/JavaNull.scala index 7b96510d4c09..82fb1ba59873 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNull.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNull.scala @@ -148,9 +148,11 @@ object JavaNull { // duplicate `Null`s at the outermost level inside `A` and `B`. val newMap = new JavaNullMap(alreadyNullable = true) derivedAndType(tp, newMap(tp1), newMap(tp2)).toJavaNullable + case tp@OrType(tp1, tp2) if !tp.isJavaNullableUnion => + val newMap = new JavaNullMap(alreadyNullable = true) + derivedOrType(tp, newMap(tp1), newMap(tp2)).toJavaNullable case tp: TypeRef if shouldNullify(tp) => tp.toJavaNullable case tp: TypeParamRef if shouldNullify(tp) => tp.toJavaNullable - case tp: OrType => tp.toJavaNullable case appTp@AppliedType(tycons, targs) => val targs2 = if (shouldDescend(appTp)) targs map this else targs derivedAppliedType(appTp, tycons, targs2).toJavaNullable From fa9e5d80f0ab90487be3a78c42dd28fdd199e522 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 14:55:02 -0500 Subject: [PATCH 117/127] Add test exercising array conversions --- tests/run/explicit-null-arrays.scala | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/run/explicit-null-arrays.scala diff --git a/tests/run/explicit-null-arrays.scala b/tests/run/explicit-null-arrays.scala new file mode 100644 index 000000000000..05b115c350bb --- /dev/null +++ b/tests/run/explicit-null-arrays.scala @@ -0,0 +1,20 @@ + +object Test { + def sort[T <: AnyRef : Ordering](a: Array[T]): Array[T] = { + import java.util.Arrays + import scala.NonNull.ArrayConversions._ + val a2: Array[T] = Arrays.copyOf(a, a.length).nn + scala.util.Sorting.quickSort(a2) + a2 + } + + def main(args: Array[String]): Unit = { + val a: Array[Integer] = Array(3, 2, 4, 1) + val a2 = sort(a) + assert(a2(0) == 1) + assert(a2(1) == 2) + assert(a2(2) == 3) + assert(a2(3) == 4) + } +} + From c06bcc9e66bdfd4db8852863867bf161062f2ddd Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 17:49:06 -0500 Subject: [PATCH 118/127] Fixes for PR --- .../src/dotty/tools/dotc/core/Contexts.scala | 1 - .../dotty/tools/dotc/core/Definitions.scala | 26 ++++++++++++------- .../src/dotty/tools/dotc/core/FlowFacts.scala | 13 +++++----- .../annotation/internal/JavaNullAnnot.scala | 8 ------ 4 files changed, 22 insertions(+), 26 deletions(-) delete mode 100644 library/src/scala/annotation/internal/JavaNullAnnot.scala diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index ab426f1bd886..3c469ca01331 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -22,7 +22,6 @@ import reporting._ import reporting.diagnostic.Message import io.AbstractFile import scala.io.Codec - import collection.mutable import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings} diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d571363c011a..81f8e91715cb 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -336,6 +336,9 @@ class Definitions { def NothingType: TypeRef = NothingClass.typeRef lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing") + /** `RefEq` is the trait defining the reference equality operators. + * It's just a marker trait and there's no corresponding class file, since it gets erased to `Object`. + */ lazy val RefEqClass: ClassSymbol = enterCompleteClassSymbol( ScalaPackageClass, tpnme.RefEq, Trait, AnyClass.typeRef :: Nil) def RefEqType: TypeRef = RefEqClass.typeRef @@ -350,21 +353,26 @@ class Definitions { def NullType: TypeRef = NullClass.typeRef lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null") + /** An alias for null values that originate in Java code. + * This type gets special treatment in the Typer. Specifically, `JavaNull` can be selected through: + * e.g. + * ``` + * // x: String|Null + * x.length // error: `Null` has no `length` field + * // x2: String|JavaNull + * x2.length // allowed by the Typer, but unsound (might throw NPE) + * ` + */ + lazy val JavaNull = enterAliasType(tpnme.JavaNull, NullType) + def JavaNullType = JavaNull.typeRef + lazy val ImplicitScrutineeTypeSym = newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef - /** Marker for null values that originate in Java. - * Equivalently defined as `type JavaNull = Null @JavaNull` - */ - lazy val JavaNull = enterAliasType(tpnme.JavaNull, AnnotatedType.make(NullType, List(Annotation(JavaNullAnnot)))) - def JavaNullType = JavaNull.typeRef - lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef") def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol - def nullable(tp: Type) = OrType(tp, NullType) - lazy val Predef_ConformsR: TypeRef = ScalaPredefModule.requiredClass("<:<").typeRef def Predef_Conforms(implicit ctx: Context): Symbol = Predef_ConformsR.symbol lazy val Predef_conformsR: TermRef = ScalaPredefModule.requiredMethodRef(nme.conforms_) @@ -834,8 +842,6 @@ class Definitions { def SetterMetaAnnot(implicit ctx: Context): ClassSymbol = SetterMetaAnnotType.symbol.asClass lazy val ShowAsInfixAnotType: TypeRef = ctx.requiredClassRef("scala.annotation.showAsInfix") def ShowAsInfixAnnot(implicit ctx: Context): ClassSymbol = ShowAsInfixAnotType.symbol.asClass - lazy val JavaNullAnnotType = ctx.requiredClassRef("scala.annotation.internal.JavaNullAnnot") - def JavaNullAnnot(implicit ctx: Context) = JavaNullAnnotType.symbol.asClass // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 7ac029a0e8a2..0249e12716ad 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -12,19 +12,18 @@ import scala.annotation.internal.sharable /** Operations on flow-sensitive type information */ object FlowFacts { - /** A set of `TermRef`s known to be non-nullable at the current program point */ + /** A set of `TermRef`s known to be non-null at the current program point */ type NonNullSet = Set[TermRef] /** The initial state where no `TermRef`s are known to be non-null */ @sharable val emptyNonNullSet = Set.empty[TermRef] - /** Is `tref` non-null (even if its info says it isn't)? */ def isNonNull(nnSet: NonNullSet, tref: TermRef): Boolean = { nnSet.contains(tref) } - /** Tries to improve de "precision" of `tpe` using flow-sensitive type information. */ + /** Try to improve the precision of `tpe` using flow-sensitive type information. */ def refineType(tpe: Type)(implicit ctx: Context): Type = tpe match { case tref: TermRef if isNonNull(ctx.nonNullFacts, tref) => NonNullTermRef.fromTermRef(tref) @@ -32,16 +31,16 @@ object FlowFacts { } /** Nullability facts inferred from a condition. - * `ifTrue` are the terms known to be non-null if the condition is true. - * `ifFalse` are the terms known to be non-null if the condition is false. + * @param ifTrue are the terms known to be non-null if the condition is true. + * @param ifFalse are the terms known to be non-null if the condition is false. */ case class Inferred(ifTrue: NonNullSet, ifFalse: NonNullSet) { - /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculates the inferred facts for + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for * (short-circuited) `e1 && e2` using De Morgan's laws. */ def combineAnd(other: Inferred): Inferred = Inferred(ifTrue.union(other.ifTrue), ifFalse.intersect(other.ifFalse)) - /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculates the inferred facts for + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for * (short-circuited) `e1 || e2` using De Morgan's laws. */ def combineOr(other: Inferred): Inferred = Inferred(ifTrue.intersect(other.ifTrue), ifFalse.union(other.ifFalse)) diff --git a/library/src/scala/annotation/internal/JavaNullAnnot.scala b/library/src/scala/annotation/internal/JavaNullAnnot.scala deleted file mode 100644 index 3036b0fec154..000000000000 --- a/library/src/scala/annotation/internal/JavaNullAnnot.scala +++ /dev/null @@ -1,8 +0,0 @@ -package scala.annotation.internal - -import scala.annotation.Annotation - -/** Marks null values originating in Java. - * TODO(abeln): give example - */ -final class JavaNullAnnot() extends Annotation From 830e97b225442217c1a7051ab0cf61a35a7b09e6 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 18:07:17 -0500 Subject: [PATCH 119/127] Add correctness proof to flow inference --- .../src/dotty/tools/dotc/core/FlowFacts.scala | 17 +++++++++++------ compiler/test/dotc/pos-test-pickling.blacklist | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 0249e12716ad..49b4a5bb1950 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -35,14 +35,19 @@ object FlowFacts { * @param ifFalse are the terms known to be non-null if the condition is false. */ case class Inferred(ifTrue: NonNullSet, ifFalse: NonNullSet) { - /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for - * (short-circuited) `e1 && e2` using De Morgan's laws. - */ + // Let `NN(e, true/false)` be the set of terms that are non-null if `e` evaluates to `true/false`. + // We can use De Morgan's laws to underapproximate `NN` via `Inferred`. + // e.g. say `e = e1 && e2`. Then if `e` is `false`, we know that either `!e1` or `!e2`. + // Let `t` be a term that is in both `NN(e1, false)` and `NN(e2, false)`. + // Then it follows that `t` must be in `NN(e, false)`. This means that if we set + // `Inferred(e1 && e2, false) = Inferred(e1, false) ∩ Inferred(e2, false)`, we'll have + // `Inferred(e1 && e2, false) ⊂ NN(e1 && e2, false)` (formally, we'd do a structural induction on `e`). + // This means that when we infer something we do so soundly. The methods below use this approach. + + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for `e1 && e2`. */ def combineAnd(other: Inferred): Inferred = Inferred(ifTrue.union(other.ifTrue), ifFalse.intersect(other.ifFalse)) - /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for - * (short-circuited) `e1 || e2` using De Morgan's laws. - */ + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for `e1 || e2`. */ def combineOr(other: Inferred): Inferred = Inferred(ifTrue.intersect(other.ifTrue), ifFalse.union(other.ifFalse)) /** The inferred facts for the negation of this condition. */ diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 293e2b5c1b0c..4f65f97d91b6 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -12,4 +12,5 @@ t3612.scala typelevel0.scala Transactions.scala explicit-null-flow.scala +explicit-null-flow2.scala t1107a.scala From 96021efb5504bed7c2a55430ed60ae643794b1da Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 18:08:50 -0500 Subject: [PATCH 120/127] Fix typo --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 81f8e91715cb..b90b85481a2f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -361,7 +361,7 @@ class Definitions { * x.length // error: `Null` has no `length` field * // x2: String|JavaNull * x2.length // allowed by the Typer, but unsound (might throw NPE) - * ` + * ``` */ lazy val JavaNull = enterAliasType(tpnme.JavaNull, NullType) def JavaNullType = JavaNull.typeRef From 6e75ea880e864455c323c0b5a33a50f2a0000aaf Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 15 Jan 2019 18:19:47 -0500 Subject: [PATCH 121/127] PR fixes --- compiler/src/dotty/tools/dotc/core/FlowFacts.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 49b4a5bb1950..394c148c74cf 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -124,7 +124,7 @@ object FlowFacts { trefOpt match { case Some(tref) => - // If `isEq`, then the condition is of the form e.g. `lhs == null`, + // If `isEq`, then the condition is of the form `lhs == null`, // in which case we know `lhs` is non-null if the condition is false. Inferred(tref, ifTrue = !isEq) case _ => emptyFacts diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e492816e5837..26f70f6c0ada 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -943,9 +943,9 @@ object Types { */ def matchesLoosely(that: Type)(implicit ctx: Context): Boolean = (this matches that) || { - var thisResult = this.widenExpr - var thatResult = that.widenExpr - (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) + val thisResult = this.widenExpr + val thatResult = that.widenExpr + (this eq thisResult) != (that eq thatResult) && (thisResult matchesLoosely thatResult) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ From ee321cfbd3ae12c502d1f7cc9630e467aeafa02c Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Thu, 17 Jan 2019 11:03:34 -0500 Subject: [PATCH 122/127] Add test --- tests/pos/explicit-null-flow2.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/pos/explicit-null-flow2.scala diff --git a/tests/pos/explicit-null-flow2.scala b/tests/pos/explicit-null-flow2.scala new file mode 100644 index 000000000000..2391da60b3be --- /dev/null +++ b/tests/pos/explicit-null-flow2.scala @@ -0,0 +1,11 @@ + +class Foo { + + val x: String|Null = ??? + val y: String|Null = ??? + val z: String|Null = ??? + + if ((x != null && z != null) || (y != null && z != null)) { + val z2: String = z + } +} From ab6934d0347cc09bae7ab60ad8add9596296fdb7 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 18 Jan 2019 11:14:15 -0500 Subject: [PATCH 123/127] Move null erasure to better location --- .../dotty/tools/dotc/core/TypeComparer.scala | 7 -- .../dotty/tools/dotc/core/TypeErasure.scala | 104 ++++++++++-------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 7835d21ee337..f50d1a9a9896 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1603,13 +1603,6 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] { * an OrType, the lub will be computed using TypeCreator#erasedLub. */ final def orType(tp1: Type, tp2: Type, isErased: Boolean = ctx.erasedTypes): Type = { - if (isErased) { - // After erasure, C | Null is just C, if C is a reference type. - // This is because types are nullable for the JVM, no matter what - // our non-nullable type system says. - if (tp1.widenDealias == defn.NullType && tp2.derivesFrom(defn.ObjectClass)) return tp2 - if (tp2.widenDealias == defn.NullType && tp1.derivesFrom(defn.ObjectClass)) return tp1 - } val t1 = distributeOr(tp1, tp2) if (t1.exists) t1 else { diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9318c9e063b2..83ee02cfa249 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -248,54 +248,62 @@ object TypeErasure { * The reason to pick last is that we prefer classes over traits that way, * which leads to more predictable bytecode and (?) faster dynamic dispatch. */ - def erasedLub(tp1: Type, tp2: Type)(implicit ctx: Context): Type = tp1 match { - case JavaArrayType(elem1) => - import dotty.tools.dotc.transform.TypeUtils._ - tp2 match { - case JavaArrayType(elem2) => - if (elem1.isPrimitiveValueType || elem2.isPrimitiveValueType) { - if (elem1.classSymbol eq elem2.classSymbol) // same primitive - JavaArrayType(elem1) - else defn.ObjectType - } else JavaArrayType(erasedLub(elem1, elem2)) - case _ => defn.ObjectType - } - case _ => - tp2 match { - case JavaArrayType(_) => defn.ObjectType - case _ => - val cls2 = tp2.classSymbol - - /** takeWhile+1 */ - def takeUntil[T](l: List[T])(f: T => Boolean): List[T] = { - @tailrec def loop(tail: List[T], acc: List[T]): List[T] = - tail match { - case h :: t => loop(if (f(h)) t else Nil, h :: acc) - case Nil => acc.reverse - } - loop(l, Nil) - } - - // We are not interested in anything that is not a supertype of tp2 - val tp2superclasses = tp1.baseClasses.filter(cls2.derivesFrom) - - // From the spec, "Linearization also satisfies the property that a - // linearization of a class always contains the linearization of its - // direct superclass as a suffix"; it's enough to consider every - // candidate up to the first class. - val candidates = takeUntil(tp2superclasses)(!_.is(Trait)) - - // Candidates st "no other common superclass or trait derives from S" - val minimums = candidates.filter { cand => - candidates.forall(x => !x.derivesFrom(cand) || x.eq(cand)) - } - - // Pick the last minimum to prioritise classes over traits - minimums.lastOption match { - case Some(lub) => valueErasure(lub.typeRef) - case _ => defn.ObjectType - } - } + def erasedLub(tp1: Type, tp2: Type)(implicit ctx: Context): Type = { + // After erasure, C | Null is just C, if C is a reference type. + // We need to short-circuit this case here because the regular lub logic below + // relies on the class hierarchy, which doesn't properly capture `Null`s subtyping + // behaviour. + if (tp1.isRefToNull && tp2.derivesFrom(defn.ObjectClass)) return tp2 + if (tp2.isRefToNull && tp1.derivesFrom(defn.ObjectClass)) return tp1 + tp1 match { + case JavaArrayType(elem1) => + import dotty.tools.dotc.transform.TypeUtils._ + tp2 match { + case JavaArrayType(elem2) => + if (elem1.isPrimitiveValueType || elem2.isPrimitiveValueType) { + if (elem1.classSymbol eq elem2.classSymbol) // same primitive + JavaArrayType(elem1) + else defn.ObjectType + } else JavaArrayType(erasedLub(elem1, elem2)) + case _ => defn.ObjectType + } + case _ => + tp2 match { + case JavaArrayType(_) => defn.ObjectType + case _ => + val cls2 = tp2.classSymbol + + /** takeWhile+1 */ + def takeUntil[T](l: List[T])(f: T => Boolean): List[T] = { + @tailrec def loop(tail: List[T], acc: List[T]): List[T] = + tail match { + case h :: t => loop(if (f(h)) t else Nil, h :: acc) + case Nil => acc.reverse + } + loop(l, Nil) + } + + // We are not interested in anything that is not a supertype of tp2 + val tp2superclasses = tp1.baseClasses.filter(cls2.derivesFrom) + + // From the spec, "Linearization also satisfies the property that a + // linearization of a class always contains the linearization of its + // direct superclass as a suffix"; it's enough to consider every + // candidate up to the first class. + val candidates = takeUntil(tp2superclasses)(!_.is(Trait)) + + // Candidates st "no other common superclass or trait derives from S" + val minimums = candidates.filter { cand => + candidates.forall(x => !x.derivesFrom(cand) || x.eq(cand)) + } + + // Pick the last minimum to prioritise classes over traits + minimums.lastOption match { + case Some(lub) => valueErasure(lub.typeRef) + case _ => defn.ObjectType + } + } + } } /** The erased greatest lower bound of two erased type picks one of the two argument types. From fcbdf0fbbc89442bc26f7fbe096371b3fc758524 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 18 Jan 2019 17:49:26 -0500 Subject: [PATCH 124/127] Comments and style fixes --- compiler/src/dotty/tools/dotc/core/Types.scala | 17 +++++++++-------- .../dotc/core/classfile/ClassfileParser.scala | 12 +++++------- .../tools/dotc/transform/FirstTransform.scala | 1 + .../dotty/tools/dotc/typer/ErrorReporting.scala | 1 - .../src/dotty/tools/dotc/typer/Implicits.scala | 4 ++++ 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 26f70f6c0ada..109906ebbe99 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -461,7 +461,7 @@ object Types { else if (rsym isSubClass lsym) lsym else if (this.isNullableUnion) { val OrType(left, _) = this.normalizeNull - // If `left` is a reference type, then the class LUB of `left | Null` is `Reference`. + // If `left` is a reference type, then the class LUB of `left | Null` is `RefEq`. // This is another one-of case that keeps this method sound, but not complete. if (left.classSymbol isSubClass defn.ObjectClass) defn.RefEqClass else NoSymbol @@ -625,6 +625,8 @@ object Types { goAnd(l, r) case tp: OrType => if (tp.isJavaNullableUnion) { + // Selecting `name` from a type `T|JavaNull` is like selecting name` from `T`. + // This can throw at runtime, but we trade soundness for usability. // We need to strip JavaNull from both the type and the prefix so that // `pre <: tp` continues to hold. tp.stripJavaNull.findMember(name, pre.stripJavaNull, required, excluded) @@ -1023,8 +1025,9 @@ object Types { case _ => this } - /** Converts types of the form `Null | T` to `T | Null`. Does not do any widening or dealiasing. */ + /** Converts types of the form `Null | T` to `T | Null`. Does not d o any widening or dealiasing. */ def normalizeNull(implicit ctx: Context): Type = { + // TODO(abeln): make normalization more robust (e.g. so it can handle nested unions). this match { case OrType(left, right) if left.isRefToNull => OrType(right, left) case _ => this @@ -1048,12 +1051,10 @@ object Types { /** Widen from singleton type to its underlying non-singleton * base type by applying one or more `underlying` dereferences. */ - final def widenSingleton(implicit ctx: Context): Type = { - stripTypeVar.stripAnnots match { - case tp: SingletonType if !tp.isOverloaded => - tp.underlying.widenSingleton - case _ => this - } + final def widenSingleton(implicit ctx: Context): Type = stripTypeVar.stripAnnots match { + case tp: SingletonType if !tp.isOverloaded => + tp.underlying.widenSingleton + case _ => this } /** Widen from TermRef to its underlying non-termref diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index fa49a2825fa9..d5dda6e38f0a 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -264,12 +264,6 @@ class ClassfileParser( addConstructorTypeParams(denot) } - def nullifyTpe(): Unit = { - val old = denot.info - denot.info = JavaNull.nullifyMember(denot.symbol, denot.info) - Printers.nullability.println(s"nullified member type from classfile for ${denot.symbol.name.show} from ${old.show} into ${denot.info.show}") - } - denot.info = pool.getType(in.nextChar) if (isEnum) denot.info = ConstantType(Constant(sym)) if (isConstructor) normalizeConstructorParams() @@ -278,7 +272,11 @@ class ClassfileParser( if (isConstructor) normalizeConstructorInfo() if ((denot is Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0) denot.info = arrayToRepeated(denot.info) - nullifyTpe() + + val old = denot.info + denot.info = JavaNull.nullifyMember(denot.symbol, denot.info) + Printers.nullability.println(s"nullified member type from classfile for ${denot.symbol.name.show} from ${old.show} into ${denot.info.show}") + // seal java enums if (isEnum) { val enumClass = sym.owner.linkedClass diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index f4c067d82665..e2aea1bf6994 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -50,6 +50,7 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = { tree match { case Select(qual, name) if !name.is(OuterSelectName) && tree.symbol.exists => + // JavaNull is already special-cased in the Typer, but needs to be handled here as well. val qualTpe = if (qual.tpe.isJavaNullableUnion) qual.tpe.stripJavaNull else qual.tpe assert( qualTpe.derivesFrom(tree.symbol.owner) || diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index ea359213cf80..11fc5ddca770 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -80,7 +80,6 @@ object ErrorReporting { if (tree.tpe.widen.exists) i"${exprStr(tree)} does not take ${kind}parameters" else { -// println(s"tree.tpe.widen = ${tree.tpe.widen.show}") i"undefined: $tree # ${tree.uniqueId}: ${tree.tpe.toString} at ${ctx.phase}" } diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 6168ac45104f..63ed70fab196 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -608,6 +608,10 @@ trait Implicits { self: Typer => val to1 = adjust(to) inferImplicit(to1, from, from.pos) match { case err: SearchFailure if !err.isAmbiguous && from.tpe.isJavaNullableUnion => + // TODO(abeln): is this worth keeping? + // When looking for an implicit conversion from A|JavaNull -> B, if we + // can't find it, then also look for an implicit conversion from A -> B. + // This is done to ease Java interop. val from1 = from.ensureConforms(from.tpe.stripJavaNull) inferImplicit(to1, from1, from.pos) case res => res From ff2be5ca214134baeffaa5c25ef499cbefa5c073 Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Fri, 18 Jan 2019 18:01:17 -0500 Subject: [PATCH 125/127] More polish --- .../src/dotty/tools/dotc/core/Types.scala | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 109906ebbe99..75587413142a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -247,7 +247,7 @@ object Types { def isRefToNull(implicit ctx: Context): Boolean = this.isRef(defn.NullClass) /** Is this (after widening and dealiasing) a type of the form `T | Null`? */ - def isNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + def isNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normNullableUnion match { case OrType(_, right) => right.isRefToNull case _ => false } @@ -272,7 +272,7 @@ object Types { } /** Is this (after widening and dealiasing) a type of the form `T | JavaNull`? */ - def isJavaNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normalizeNull match { + def isJavaNullableUnion(implicit ctx: Context): Boolean = this.widenDealias.normNullableUnion match { case OrType(_, right) => right.isJavaNullType case _ => false } @@ -460,7 +460,7 @@ object Types { if (lsym isSubClass rsym) rsym else if (rsym isSubClass lsym) lsym else if (this.isNullableUnion) { - val OrType(left, _) = this.normalizeNull + val OrType(left, _) = this.normNullableUnion // If `left` is a reference type, then the class LUB of `left | Null` is `RefEq`. // This is another one-of case that keeps this method sound, but not complete. if (left.classSymbol isSubClass defn.ObjectClass) defn.RefEqClass @@ -1014,19 +1014,19 @@ object Types { } /** Strips the nullability from a type: `T | Null` goes to `T` */ - def stripNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { + def stripNull(implicit ctx: Context): Type = this.widenDealias.normNullableUnion match { case OrType(left, right) if right.isRefToNull => left.stripNull case _ => this } - /** Strips the java nullability from a type: `T | JavaNull` goes to `T` */ - def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normalizeNull match { + /** Strips the Java nullability from a type: `T | JavaNull` goes to `T` */ + def stripJavaNull(implicit ctx: Context): Type = this.widenDealias.normNullableUnion match { case OrType(left, right) if right.isJavaNullType => left.stripJavaNull case _ => this } - /** Converts types of the form `Null | T` to `T | Null`. Does not d o any widening or dealiasing. */ - def normalizeNull(implicit ctx: Context): Type = { + /** Normalizes nullable unions so that `Null` is the last operand (e.g. `Null|T` goes to `T|Null`) */ + def normNullableUnion(implicit ctx: Context): Type = { // TODO(abeln): make normalization more robust (e.g. so it can handle nested unions). this match { case OrType(left, right) if left.isRefToNull => OrType(right, left) @@ -1052,8 +1052,7 @@ object Types { * base type by applying one or more `underlying` dereferences. */ final def widenSingleton(implicit ctx: Context): Type = stripTypeVar.stripAnnots match { - case tp: SingletonType if !tp.isOverloaded => - tp.underlying.widenSingleton + case tp: SingletonType if !tp.isOverloaded => tp.underlying.widenSingleton case _ => this } @@ -1102,10 +1101,8 @@ object Types { def widenUnion(implicit ctx: Context): Type = this match { case OrType(tp1, tp2) => if (isNullableUnion) { - normalizeNull match { - case OrType(leftTpe, nullTpe) => OrType(leftTpe.widenUnion, nullTpe) - case tpe => assert(false, s"Expected a union type, but got ${tpe.show}"); tpe - } + val OrType(leftTpe, nullTpe) = normNullableUnion + OrType(leftTpe.widenUnion, nullTpe) } else { ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match { case union: OrType => union.join @@ -1780,7 +1777,7 @@ object Types { // This is a hack to detect whether `this` is an instance of `NonNullTermRef`, and adjust // the denotation accordingly. The "adjustment" happens in `computeDenot`, which is currently // marked as private. - // Figure out a different code structure. + // Overriden in `NonNullTermRef`. protected val isNonNull: Boolean = false // Invariants: @@ -2379,7 +2376,6 @@ object Types { // We need to set the non-null denotation directly because normally the "non-nullable" denotations // are created in `computeDenot`, but they _won't_ be computed if the original `tref` _already_ had // a cached denotation. - // This is brittle, since `This` nn.withDenot(denot) } } @@ -4344,7 +4340,7 @@ object Types { case _ => NoType } - + def isInstantiatable(tp: Type)(implicit ctx: Context): Boolean = zeroParamClass(tp) match { case cinfo: ClassInfo => val selfType = cinfo.selfType.asSeenFrom(tp, cinfo.cls) From 8823c43d60c8a903c2b6cbc9490c4806a46404cc Mon Sep 17 00:00:00 2001 From: Hoang Phan Date: Tue, 22 Jan 2019 07:48:48 -0500 Subject: [PATCH 126/127] Modified test --- tests/run/t3126.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/run/t3126.scala b/tests/run/t3126.scala index 36322bf89696..f91ba48c19b7 100644 --- a/tests/run/t3126.scala +++ b/tests/run/t3126.scala @@ -1,9 +1,12 @@ object Test { case class C(x: Int) - val v: Some[Int] = null + val v1: Some[Int] | Null = null + val v2: Some[Int] = null.asInstanceOf[Some[Int]] def main(args: Array[String]): Unit = { - try C.unapply(null) catch { case _: MatchError => } - try v match { case Some(1) => } catch { case _: MatchError => } + try C.unapply(null.asInstanceOf[C]) catch { case _: MatchError => } + try (v1: @unchecked) match { case Some(1) => } catch { case _: MatchError => } + try (v2: @unchecked) match { case Some(1) => } catch { case _: NullPointerException => } } } + From fc56d9e8398e30b5073e546f57e5bf7a4a22334d Mon Sep 17 00:00:00 2001 From: Abel Nieto Date: Tue, 22 Jan 2019 17:28:09 -0500 Subject: [PATCH 127/127] Expand flow sensitive inference This improves flow sensitive inference so that it handles a bunch of previously-unsupported cases: * conditions inside blocks * inlined code * isInstanceOf checks * reference equality checks: eq and ne --- .../src/dotty/tools/dotc/core/FlowFacts.scala | 21 +++++++++++----- .../test/dotc/pos-test-pickling.blacklist | 2 ++ tests/neg/explicit-null-flow2.scala | 18 ++++++++++++++ tests/neg/explicit-null-flow3.scala | 17 +++++++++++++ tests/neg/explicit-null-flow4.scala | 18 ++++++++++++++ tests/pos/explicit-null-flow3.scala | 9 +++++++ tests/pos/explicit-null-flow4.scala | 24 +++++++++++++++++++ 7 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 tests/neg/explicit-null-flow2.scala create mode 100644 tests/neg/explicit-null-flow3.scala create mode 100644 tests/neg/explicit-null-flow4.scala create mode 100644 tests/pos/explicit-null-flow3.scala create mode 100644 tests/pos/explicit-null-flow4.scala diff --git a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala index 394c148c74cf..5e28fa9f7ffc 100644 --- a/compiler/src/dotty/tools/dotc/core/FlowFacts.scala +++ b/compiler/src/dotty/tools/dotc/core/FlowFacts.scala @@ -1,8 +1,10 @@ package dotty.tools.dotc.core -import dotty.tools.dotc.ast.Trees.{Apply, Literal, Select} import dotty.tools.dotc.ast.tpd._ import StdNames.nme +import dotty.tools.dotc.ast.Trees.{Tree => _, _} +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.Types.{NonNullTermRef, TermRef, Type} @@ -78,8 +80,6 @@ object FlowFacts { * TODO(abeln): add longer description of the algorithm */ def inferNonNull(cond: Tree)(implicit ctx: Context): Inferred = { - val emptyFacts = Inferred(emptyNonNullSet, emptyNonNullSet) - /** Combine two sets of facts according to `op`. */ def combine(lhs: Inferred, op: Name, rhs: Inferred): Inferred = { op match { @@ -88,15 +88,24 @@ object FlowFacts { } } + val emptyFacts = Inferred(emptyNonNullSet, emptyNonNullSet) + val nullLit = tpd.Literal(Constant(null)) + /** Recurse over a conditional to extract flow facts. */ def recur(tree: Tree): Inferred = { tree match { case Apply(Select(lhs, op), List(rhs)) => if (op == nme.ZAND || op == nme.ZOR) combine(recur(lhs), op, recur(rhs)) - else if (op == nme.EQ || op == nme.NE) newFact(lhs, isEq = op == nme.EQ, rhs) + else if (op == nme.EQ || op == nme.NE || op == nme.eq || op == nme.ne) newFact(lhs, isEq = (op == nme.EQ || op == nme.eq), rhs) else emptyFacts - case Select(lhs, neg) if neg == nme.UNARY_! => - recur(lhs).negate + case TypeApply(Select(lhs, op), List(targ)) if op == nme.isInstanceOf_ && targ.tpe.isRefToNull => + // TODO(abeln): handle type test with argument that's not a subtype of `Null`. + // We could infer "non-null" in that case: e.g. `if (x.isInstanceOf[String]) { // x can't be null }` + newFact(lhs, isEq = true, nullLit) + case Select(lhs, neg) if neg == nme.UNARY_! => recur(lhs).negate + case Block(_, expr) => recur(expr) + case Inlined(_, _, expansion) => recur(expansion) + case Typed(expr, _) => recur(expr) // TODO(abeln): check that the type is `Boolean`? case _ => emptyFacts } } diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 4f65f97d91b6..c43b064b77b2 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -13,4 +13,6 @@ typelevel0.scala Transactions.scala explicit-null-flow.scala explicit-null-flow2.scala +explicit-null-flow3.scala +explicit-null-flow4.scala t1107a.scala diff --git a/tests/neg/explicit-null-flow2.scala b/tests/neg/explicit-null-flow2.scala new file mode 100644 index 000000000000..7ac243f7fd36 --- /dev/null +++ b/tests/neg/explicit-null-flow2.scala @@ -0,0 +1,18 @@ + +// Test that flow inference can handle blocks. +class Foo { + val x: String|Null = "hello" + if ({val z = 10; {1 + 1 == 2; x != null}}) { + val l = x.length + } + + if ({x != null; true}) { + val l = x.length // error + } + + val x2: String|Null = "world" + if ({{{{1 + 1 == 2; x != null}}}} && x2 != null) { + val l = x.length + val l2 = x2.length + } +} diff --git a/tests/neg/explicit-null-flow3.scala b/tests/neg/explicit-null-flow3.scala new file mode 100644 index 000000000000..59386cf34c07 --- /dev/null +++ b/tests/neg/explicit-null-flow3.scala @@ -0,0 +1,17 @@ + +// Test that flow inference handles `isInstanceOf` checks. +class Foo { + val x: String|Null = "" + if (!x.isInstanceOf[Null]) { + val y = x.length + } + + if (x.isInstanceOf[Null]) { + } else { + val y = x.length + } + + if (x.isInstanceOf[String]) { + val y = x.length // error: we only infer if the type argument is `Null` + } +} diff --git a/tests/neg/explicit-null-flow4.scala b/tests/neg/explicit-null-flow4.scala new file mode 100644 index 000000000000..079f8c97dd1b --- /dev/null +++ b/tests/neg/explicit-null-flow4.scala @@ -0,0 +1,18 @@ + +// Test that flow inference handles `eq/ne` checks. +class Foo { + val x: String|Null = "" + + if (x eq null) { + val y = x.length // error + } else { + val y = x.length + } + + + if (x ne null) { + val y = x.length + } else { + val y = x.length // error + } +} diff --git a/tests/pos/explicit-null-flow3.scala b/tests/pos/explicit-null-flow3.scala new file mode 100644 index 000000000000..21198e09a748 --- /dev/null +++ b/tests/pos/explicit-null-flow3.scala @@ -0,0 +1,9 @@ + +// Test that flow inference can look inside type ascriptions. +// This is useful in combination with inlining (because inlined methods have an ascribed return type). +class Foo { + val x: String|Null = "hello" + if ((x != null): Boolean) { + val y = x.length + } +} diff --git a/tests/pos/explicit-null-flow4.scala b/tests/pos/explicit-null-flow4.scala new file mode 100644 index 000000000000..ef1c8b0c7ab9 --- /dev/null +++ b/tests/pos/explicit-null-flow4.scala @@ -0,0 +1,24 @@ + +// This test is based on tests/pos/rbtree.scala +// and it tests that we can use an inline method to "abstract" a more complicated +// isInstanceOf check, while at the same time getting the flow inference to know +// that `isRedTree(tree) => tree ne null`. +class TreeOps { + abstract class Tree[A, B](val key: A, val value: B) + class RedTree[A, B](override val key: A, override val value: B) extends Tree[A, B](key, value) + + private[this] inline def isRedTree(tree: Tree[_, _]) = (tree ne null) && tree.isInstanceOf[RedTree[_, _]] + + def foo[A, B](tree: Tree[A, B]): Unit = { + if (isRedTree(tree)) { + val key = tree.key + val value = tree.value + } + + if (!isRedTree(tree)) { + } else { + val key = tree.key + val value = tree.value + } + } +}