Skip to content

[WIP] Scala with Explicit Nulls #5747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 127 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
c576680
Make Null a subtype of only Any and AnyRef
abeln Aug 10, 2018
2a22e7a
Automatically replace "null" by "???" in positive tests
abeln Aug 21, 2018
b5afd26
Change typing of "throw" expressions to allow a nullable argument
abeln Aug 21, 2018
3eb3874
Revert improperly modified test
abeln Aug 21, 2018
fbfbd4f
Addititional automated conversion of null to ???
abeln Aug 22, 2018
8ebd3eb
Fix additional positive tests
abeln Aug 22, 2018
73ffa5d
Revert incorrect change to backend interface
abeln Aug 24, 2018
edc6320
Implement Java nullability transform
abeln Aug 22, 2018
e4ee72c
Refactor tests and add new source test
abeln Aug 27, 2018
dc9601d
Add JavaNull type
abeln Aug 28, 2018
1217b9b
Nullify applied types too
abeln Aug 28, 2018
16e4af8
Handle JavaNull when converting varargs
abeln Aug 29, 2018
1f99138
Use helper methods when typing Select trees
abeln Aug 29, 2018
01300e0
Better implementation of member selection for JavaNull
abeln Aug 30, 2018
5701392
Teach FirstTransform that |JavaNull is see-through
abeln Aug 30, 2018
f6d116b
Improve nullability transform
abeln Aug 30, 2018
eda351b
Pass empty string instead of null in dummy constructor
abeln Aug 30, 2018
8ffaea3
Moar easy test fixes
abeln Aug 30, 2018
e0d46d9
Make reference types nullable again after erasure
abeln Aug 31, 2018
8c83873
Fix #5067: Ycheck failure in pattern matching against a value of type…
abeln Sep 3, 2018
9ee807c
Breakup test suite into smaller tests
abeln Sep 7, 2018
3f8d949
Handle constructors properly in nullability transform
abeln Sep 10, 2018
b048428
Better check for whether we're before erasure
abeln Sep 10, 2018
9491c16
Add test for nullifying methods
abeln Sep 10, 2018
270960c
Don't nullify the inside of Class[] and the special TYPE field
abeln Sep 10, 2018
2b4a7d6
Fix a couple of failing tests
abeln Sep 14, 2018
9a86c34
Fix two more tests
abeln Sep 17, 2018
9f4a6d4
Handle repeated parameters
abeln Sep 17, 2018
c1df4c7
Fix another test
abeln Sep 17, 2018
02cefbc
Don't nullify the return type of toString
abeln Sep 17, 2018
09ea9a1
Add two explicit null tests
abeln Oct 22, 2018
ed13029
Fix tests/pos/annotations2.scala
abeln Oct 22, 2018
4c4cce7
Fix tests/pos/tcpoly_seq.scala
abeln Oct 22, 2018
b23a3e6
Add mechanism to except fields/methods from the transform
abeln Oct 22, 2018
d0ca905
Fix tests/pos/t4579.scala
abeln Oct 22, 2018
406dcb3
Fix tests/pos/extractors.scala
abeln Oct 22, 2018
266e92f
Fix tests/pos/Meter.scala and tests/pos/extmethods.scala
abeln Oct 22, 2018
786038d
Factor out java nullability transform into its own file
abeln Oct 22, 2018
528dce0
Fixed tests/pos/explicitOuter.scala
abeln Oct 22, 2018
10e1454
Fix tests/pos/i2732.scala
abeln Oct 22, 2018
31e9bc3
Revert changes to equality
abeln Oct 25, 2018
c8a95af
Fix tests/pos/i1754.scala
abeln Oct 25, 2018
66b7801
Fix tests/pos/virtpatmat_gadt_array.scala
abeln Oct 25, 2018
04764fc
Fix tests/pos/t1001.scala
abeln Oct 25, 2018
ef6cf46
Fix some more tests
abeln Oct 25, 2018
53bdbc4
Fix two more tests
abeln Oct 30, 2018
2c4e991
Fix tests/pos/i1044.scala
abeln Oct 30, 2018
06caedd
Allow prototypes of the form T|Null to be used for type inference
abeln Nov 6, 2018
b62eef1
Improve naming of nullability convenience functions
abeln Nov 6, 2018
24a06b9
Fix positive tests
abeln Nov 12, 2018
f4c3695
Fix moar tests
abeln Nov 12, 2018
f1cce5a
Better prototypes for function literals
abeln Nov 12, 2018
456191a
Don't propagate nullability inside Java generics
abeln Nov 13, 2018
b384d2c
Don't widen union types of the form `T|Null`
abeln Nov 13, 2018
a6cd806
Fix broken test
abeln Nov 13, 2018
1a7ad57
Fix tests
abeln Nov 13, 2018
6239966
Better handling of SAM types and nullability
abeln Nov 14, 2018
26714fe
Ignore nulls when doing override checks
abeln Nov 15, 2018
3b9ecd3
Take 2 at ignoring JavaNull in override checks
abeln Nov 16, 2018
02f8bd4
Add additional case to override test
abeln Nov 16, 2018
b0dc6b1
Add .nn extension method to strip away nullability
abeln Nov 22, 2018
78556d7
Strip away JavaNull during implicit conversion search
abeln Nov 22, 2018
89b920b
Fix a few tests
abeln Nov 22, 2018
ccdba0e
Moar tests
abeln Nov 23, 2018
cae4ef5
Don't use null literal in parser error terms
abeln Nov 23, 2018
17b26ae
A few more
abeln Nov 23, 2018
822522a
Fix tests
abeln Nov 26, 2018
887d4c0
One more
abeln Nov 26, 2018
47b51c0
Make it clear that we disallow comparing null against value types
abeln Nov 26, 2018
1fa226c
3 more tests
abeln Nov 26, 2018
f611bf4
Test that we can call .nn on a non-null type
abeln Nov 26, 2018
eb47a28
Fix lots of tests
abeln Nov 29, 2018
47196c4
A couple more tests
abeln Nov 29, 2018
d0adc48
[WIP] Flow-sensitive type inference for nullability
abeln Dec 5, 2018
d027135
Add basic test for flow inference
abeln Dec 5, 2018
7d1c3ba
Better flow-sensitive type inference
abeln Dec 6, 2018
1aa0a1b
Add flow inference inside boolean conditions
abeln Dec 7, 2018
380ed44
Add test case using unary neg
abeln Dec 7, 2018
03539f4
Fix rebase errors
abeln Dec 7, 2018
3d8af6a
Don't generate fallback Eq in Null case
abeln Dec 7, 2018
e258fb1
Fix a few tests
abeln Dec 10, 2018
d47f825
Fix a bunch of tests
abeln Dec 10, 2018
140e528
More robust id of JavaNull alias
abeln Dec 10, 2018
96f9af4
fix tests
abeln Dec 13, 2018
2266821
Teach space checks that reference types are non-nullable
abeln Dec 13, 2018
198c631
fix tests
abeln Dec 13, 2018
1e0407a
When collecting nullable fields, use old notion of nullability
abeln Dec 13, 2018
94770ae
fix tests
abeln Dec 13, 2018
5899029
fix test
abeln Dec 13, 2018
6f9ba24
fix tests
abeln Dec 17, 2018
a047492
fix tests
abeln Dec 17, 2018
03435d5
fix tests
abeln Dec 17, 2018
6ef2d32
fix test
abeln Dec 18, 2018
5ab29f1
Make .nn throw a NPE if the underlying value is null
abeln Dec 18, 2018
774e481
fix test
abeln Dec 18, 2018
b89a54f
Add implicit conversions for nullable arrays
abeln Dec 18, 2018
62a3c47
fix tests
abeln Dec 18, 2018
ed25729
fix tests
abeln Dec 18, 2018
fedb587
fix test
abeln Dec 18, 2018
00c893e
fix test
abeln Dec 19, 2018
c9d51f7
Better handling of SAM types
abeln Dec 19, 2018
48fef33
add comment
abeln Dec 19, 2018
69811e5
Add implicit conversions from nullable array
abeln Jan 7, 2019
64146a7
Fix bugs introduced when adding support for repeated params
abeln Jan 7, 2019
486dda4
fix test
abeln Jan 7, 2019
039259f
fix test
abeln Jan 7, 2019
6755bbd
Fix bug when expanding SAM types
abeln Jan 7, 2019
42b49a4
fix tests
abeln Jan 7, 2019
e4e66df
fix tests
abeln Jan 7, 2019
f5fcaa0
fix typo
abeln Jan 8, 2019
8bd7899
Make Null a subtype of Any, instead of AnyRef
abeln Jan 11, 2019
67c72ff
Revert changes to overriding
abeln Jan 14, 2019
253c36a
Handle intersections in Java null transform
abeln Jan 14, 2019
246f70b
Fix tests
abeln Jan 14, 2019
320e38a
Fix array tests to use implicit conversions
abeln Jan 15, 2019
5b01205
More robust handling of union types in JavaNull transform
abeln Jan 15, 2019
fa9e5d8
Add test exercising array conversions
abeln Jan 15, 2019
c06bcc9
Fixes for PR
abeln Jan 15, 2019
830e97b
Add correctness proof to flow inference
abeln Jan 15, 2019
96021ef
Fix typo
abeln Jan 15, 2019
6e75ea8
PR fixes
abeln Jan 15, 2019
ee321cf
Add test
abeln Jan 17, 2019
ab6934d
Move null erasure to better location
abeln Jan 18, 2019
fcbdf0f
Comments and style fixes
abeln Jan 18, 2019
ff2be5c
More polish
abeln Jan 18, 2019
8823c43
Modified test
petrpan26 Jan 22, 2019
fc56d9e
Expand flow sensitive inference
abeln Jan 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)*/
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/config/JavaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ object Printers {
val typr: Printer = noPrinter
val unapp: Printer = noPrinter
val variances: Printer = noPrinter
val nullability: Printer = noPrinter
}
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ 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
Expand Down Expand Up @@ -142,6 +142,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
Expand Down Expand Up @@ -487,6 +492,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(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 }
Expand Down Expand Up @@ -563,6 +570,7 @@ object Contexts {
typeComparer = new TypeComparer(this)
searchHistory = new SearchRoot
gadt = EmptyGADTMap
nonNullFacts = FlowFacts.emptyNonNullSet
}

@sharable object NoContext extends Context {
Expand Down
66 changes: 50 additions & 16 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -327,23 +328,48 @@ 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))
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

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")

/** 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


lazy val ScalaPredefModuleRef: TermRef = ctx.requiredModuleRef("scala.Predef")
def ScalaPredefModule(implicit ctx: Context): Symbol = ScalaPredefModuleRef.symbol

Expand Down Expand Up @@ -962,10 +988,16 @@ 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) = {
// After erasure, reference types become nullable again.
if (!ctx.phase.erasedTypes) cls == NothingClass
else cls == NothingClass || cls == NullClass
}
def isBottomType(tp: Type) = {
// After erasure, reference types become nullable again.
if (!ctx.phase.erasedTypes) tp.derivesFrom(NothingClass)
else tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)
}

/** Is a function class.
* - FunctionN for N >= 0
Expand Down Expand Up @@ -1054,7 +1086,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] =
Expand All @@ -1069,7 +1102,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
Expand Down Expand Up @@ -1257,13 +1290,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,
Expand All @@ -1280,7 +1314,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

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Flags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
166 changes: 166 additions & 0 deletions compiler/src/dotty/tools/dotc/core/FlowFacts.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package dotty.tools.dotc.core

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}

import scala.annotation.internal.sharable

/** Operations on flow-sensitive type information */
object FlowFacts {

/** 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)
}

/** 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)
case _ => tpe
}

/** Nullability facts inferred from a condition.
* @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) {
// 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 `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. */
def negate: Inferred = Inferred(ifFalse, ifTrue)
}

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 = {
/** Combine two sets of facts according to `op`. */
def combine(lhs: Inferred, op: Name, rhs: Inferred): Inferred = {
op match {
case _ if op == nme.ZAND => lhs.combineAnd(rhs)
case _ if op == nme.ZOR => lhs.combineOr(rhs)
}
}

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 || op == nme.eq || op == nme.ne) newFact(lhs, isEq = (op == nme.EQ || op == nme.eq), rhs)
else emptyFacts
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
}
}

/** 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) =>
// 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
}
}

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
}
}
}
Loading