Skip to content

Dotty with explicit nulls #6344

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 75 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
1c5679b
Add an explicit nulls mode to Dotty
abeln Apr 4, 2019
07d1882
remove ValDefInBlockCompleter
noti0na1 Sep 19, 2019
9912940
add comments
noti0na1 Sep 20, 2019
36aca35
remove implicit check for ValDef
noti0na1 Sep 23, 2019
46c3e16
refine comments
noti0na1 Sep 24, 2019
e43963b
modify flow typing for all defs
noti0na1 Sep 25, 2019
4f9a437
fix flow typing when forward referred
noti0na1 Sep 26, 2019
19c5e40
adjust tests; add comments
noti0na1 Sep 27, 2019
b3c85e3
Handle Java arguments with 'Object' type in override checks
abeln Sep 27, 2019
3f2e8d1
Add test for unboxed option type
abeln Sep 27, 2019
e2588b7
Merge branch 'master' into dotty-explicit-nulls
abeln Sep 27, 2019
4fd2085
Use flatMap in opaque option test
abeln Sep 27, 2019
a56557f
refine comments
noti0na1 Sep 27, 2019
0a54fe6
Merge pull request #26 from noti0na1/dotty-explicit-nulls
noti0na1 Sep 27, 2019
902dc4a
Small changes according to comments
noti0na1 Sep 27, 2019
1168856
cache flow facts on trees
noti0na1 Sep 30, 2019
2d5d784
move FlowFactsOnTree to Typer; refine comments
noti0na1 Oct 1, 2019
f72f201
move maybeNull
noti0na1 Oct 2, 2019
de5241b
rename to maybeNullable
noti0na1 Oct 2, 2019
8e900f1
Merge pull request #27 from noti0na1/dotty-explicit-nulls-small
noti0na1 Oct 2, 2019
92b6c5c
Handle Java varargs during nullification
abeln Oct 7, 2019
feb11b0
modify methods in NullOpsDecorator
noti0na1 Oct 2, 2019
0000c28
rename and adjust methods
noti0na1 Oct 2, 2019
aabb4e1
update nullnull test
noti0na1 Oct 2, 2019
712e2ca
simplify the result of normNullableUnion
noti0na1 Oct 4, 2019
55db452
remove extra space
noti0na1 Oct 4, 2019
9595870
optimize widenUnion
noti0na1 Oct 4, 2019
7663fc2
rewrite widenUnion
noti0na1 Oct 7, 2019
53a88cd
further simplify widenUnion
noti0na1 Oct 8, 2019
fdea080
add comment
noti0na1 Oct 8, 2019
19925a5
Merge pull request #28 from noti0na1/dotty-explicit-nulls-ops
noti0na1 Oct 8, 2019
f9ca273
inline policies in JavaNullInterop
noti0na1 Oct 3, 2019
92095a2
remove extra braces
noti0na1 Oct 8, 2019
0b35a43
combine two type mapper
noti0na1 Oct 8, 2019
c7396db
remove toJavaNullableUnion
noti0na1 Oct 9, 2019
28552f5
add a helper for OrType(tp, JavaNull)
noti0na1 Oct 9, 2019
d4df9e7
Merge pull request #29 from noti0na1/dotty-explicit-nulls-small
noti0na1 Oct 9, 2019
e2c47d7
null is not allowed to be thrown; small edit to the code style
noti0na1 Oct 11, 2019
ec880ad
change type eq test in isNullSpace
noti0na1 Oct 11, 2019
513890d
edit comment
noti0na1 Oct 15, 2019
6c898c7
update comment on flags
noti0na1 Oct 17, 2019
fa43361
Merge pull request #30 from noti0na1/dotty-explicit-nulls-small
noti0na1 Oct 17, 2019
919f5c6
Merge remote-tracking branch 'upstream/master' into dotty-explicit-nulls
noti0na1 Oct 17, 2019
74e1ede
Merge pull request #31 from noti0na1/dotty-explicit-nulls
abeln Oct 17, 2019
6b25d48
revert change to throwMethod; clean up if-else
noti0na1 Oct 17, 2019
70b5df0
Merge pull request #32 from noti0na1/dotty-explicit-nulls
noti0na1 Oct 18, 2019
6e92146
add notnull detect
noti0na1 Oct 18, 2019
9b6846e
fix cyclicref
noti0na1 Oct 21, 2019
e0bc5d3
Merge remote-tracking branch 'upstream/master' into dotty-explicit-nu…
noti0na1 Oct 22, 2019
cbb876c
change NotNull string list to ClassSymbol list
noti0na1 Oct 22, 2019
750dd50
ensure NotNull works for fields
noti0na1 Oct 22, 2019
183d237
Merge pull request #34 from noti0na1/dotty-explicit-nulls-small
abeln Oct 23, 2019
0cbfa6b
Merge remote-tracking branch 'explicit-nulls/dotty-explicit-nulls' in…
noti0na1 Oct 23, 2019
c960acf
Optimize JavaNullInterop; add tests for Java files; remove dummy notn…
noti0na1 Oct 23, 2019
b0acf92
rename variables
noti0na1 Oct 24, 2019
9365e1b
add generic tests
noti0na1 Oct 24, 2019
ca62bf9
fix typos and indents
noti0na1 Oct 25, 2019
a49d4c3
remove redundent comment
noti0na1 Oct 25, 2019
8481c2a
update comments
noti0na1 Oct 25, 2019
ae4253a
update comments and var names
noti0na1 Oct 28, 2019
ad9ae9b
Merge pull request #33 from noti0na1/dotty-explicit-nulls
noti0na1 Oct 28, 2019
9913410
merge dotty upstream
noti0na1 Oct 28, 2019
313057c
Merge pull request #35 from noti0na1/dotty-explicit-nulls
abeln Oct 29, 2019
3a1d17c
fix nullifying type arguements
noti0na1 Oct 31, 2019
303d2de
fix indent
noti0na1 Oct 31, 2019
9d77d64
fix typo in the test
noti0na1 Nov 4, 2019
cd623d4
Merge pull request #37 from noti0na1/dotty-explicit-nulls
noti0na1 Nov 4, 2019
54eca3c
merge upstream changes
noti0na1 Nov 4, 2019
8c195dc
fix syntax error
noti0na1 Nov 4, 2019
946a7a9
Merge pull request #38 from noti0na1/dotty-explicit-nulls
abeln Nov 4, 2019
856ba31
change the way of creating ClassSymbol; add neg test for annotation
noti0na1 Nov 6, 2019
4cd1114
update comments
noti0na1 Nov 7, 2019
4a8b754
Merge pull request #41 from noti0na1/dotty-explicit-nulls
noti0na1 Nov 7, 2019
6d6d2ec
Merge remote-tracking branch 'upstream/master' into dotty-explicit-nulls
noti0na1 Nov 7, 2019
944f91d
Merge pull request #42 from noti0na1/dotty-explicit-nulls
abeln Nov 7, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class ScalaSettings extends Settings.SettingGroup {

// Extremely experimental language features
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Enable kind polymorphism (see https://dotty.epfl.ch/docs/reference/kind-polymorphism.html). Potentially unsound.")
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")

/** Area-specific debug output */
val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.")
Expand Down
15 changes: 14 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import xsbti.AnalysisCallback
import plugins._
import java.util.concurrent.atomic.AtomicInteger

import dotty.tools.dotc.core.FlowTyper.FlowFacts

object Contexts {

private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
Expand All @@ -47,7 +49,8 @@ object Contexts {
private val (compilationUnitLoc, store6) = store5.newLocation[CompilationUnit]()
private val (runLoc, store7) = store6.newLocation[Run]()
private val (profilerLoc, store8) = store7.newLocation[Profiler]()
private val initialStore = store8
private val (flowFactsLoc, store9) = store8.newLocation[FlowFacts](FlowTyper.emptyFlowFacts)
private val initialStore = store9

/** A context is passed basically everywhere in dotc.
* This is convenient but carries the risk of captured contexts in
Expand Down Expand Up @@ -207,6 +210,9 @@ object Contexts {
/** The current compiler-run profiler */
def profiler: Profiler = store(profilerLoc)

/** The terms currently known to be non-null (in spite of their declared type) */
def flowFacts: FlowFacts = store(flowFactsLoc)

/** The new implicit references that are introduced by this scope */
protected var implicitsCache: ContextualImplicits = null
def implicits: ContextualImplicits = {
Expand Down Expand Up @@ -421,6 +427,9 @@ object Contexts {
def useColors: Boolean =
base.settings.color.value == "always"

/** Is the explicit nulls option set? */
def explicitNulls: Boolean = base.settings.YexplicitNulls.value

protected def init(outer: Context, origin: Context): this.type = {
util.Stats.record("Context.fresh")
_outer = outer
Expand Down Expand Up @@ -556,6 +565,10 @@ object Contexts {
def setRun(run: Run): this.type = updateStore(runLoc, run)
def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler)
def setFreshNames(freshNames: FreshNameCreator): this.type = updateStore(freshNamesLoc, freshNames)
def addFlowFacts(facts: FlowFacts): this.type = {
assert(settings.YexplicitNulls.value)
updateStore(flowFactsLoc, store(flowFactsLoc) ++ facts)
}

def setProperty[T](key: Key[T], value: T): this.type =
setMoreProperties(moreProperties.updated(key, value))
Expand Down
105 changes: 83 additions & 22 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package core

import scala.annotation.{threadUnsafe => tu}
import Types._, Contexts._, Symbols._, SymDenotations._, StdNames._, Names._
import Flags._, Scopes._, Decorators._, NameOps._, Periods._
import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._
import unpickleScala2.Scala2Unpickler.ensureConstructor
import scala.collection.mutable
import collection.mutable
Expand Down Expand Up @@ -278,7 +278,7 @@ class Definitions {
@tu 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, AnyType :: Nil, newScope)
cls.setFlag(NoInits | JavaDefined)

// The companion object doesn't really exist, so it needs to be marked as
Expand All @@ -295,8 +295,16 @@ class Definitions {
@tu lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType)
def AnyRefType: TypeRef = AnyRefAlias.typeRef

@tu lazy val Object_eq: TermSymbol = enterMethod(ObjectClass, nme.eq, methOfAnyRef(BooleanType), Final)
@tu lazy val Object_ne: TermSymbol = enterMethod(ObjectClass, nme.ne, methOfAnyRef(BooleanType), Final)
@tu lazy val Object_eq: TermSymbol = {
// If explicit nulls is enabled, then we want to allow `(x: String).eq(null)`, so we need
// to adjust the signature of `eq` accordingly.
enterMethod(ObjectClass, nme.eq, methOfAnyRefOrNull(BooleanType), Final)
}
@tu lazy val Object_ne: TermSymbol = {
// If explicit nulls is enabled, then we want to allow `(x: String).ne(null)`, so we need
// to adjust the signature of `ne` accordingly.
enterMethod(ObjectClass, nme.ne, methOfAnyRefOrNull(BooleanType), Final)
}
@tu lazy val Object_synchronized: TermSymbol = enterPolyMethod(ObjectClass, nme.synchronized_, 1,
pt => MethodType(List(pt.paramRefs(0)), pt.paramRefs(0)), Final)
@tu lazy val Object_clone: TermSymbol = enterMethod(ObjectClass, nme.clone_, MethodType(Nil, ObjectType), Protected)
Expand Down Expand Up @@ -336,11 +344,29 @@ class Definitions {
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef))
def NothingType: TypeRef = NothingClass.typeRef
@tu lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing")
@tu lazy val NullClass: ClassSymbol = enterCompleteClassSymbol(
ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef))
@tu lazy val NullClass: ClassSymbol = {
val parent = if (ctx.explicitNulls) AnyType else ObjectType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try AnyVal instead of Any for the parent of Null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An issue with Having Null under AnyVal is that NotNull is harder to express. So, maybe hold off with that for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, what NotNull are you referring to? With Null under AnyVal, a type T is guaranteed not to contain null if T <: AnyRef. It's as simple as that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type could also be a primitive type like Int or Boolean, or a value class. NotNull is needed for type signatures like a Java-like map get:

def [K, V <: NotNull](m: Map[K, V]) get (k: K): V | Null

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see.

Well TBH I'm not sure that using this kind of signature is something we would want to encourage. We want people not to use Null in a normal Scala program.

Anyway, you can still define NotNull as

type NotNull = AnyRef | Boolean | Char | Byte | Short | Int | Long | Float | Double | Unit

Yes, that would take longer to typecheck against, but I would certainly hope that usage of NotNull in a regular Scala program would be very limited, and used only when performance is so important that you're willing to deal with Null. And if you're concerned about performance, then probably you're concerned about boxing and that means you probably don't want to accept other primitive types, so <: AnyRef is what you would use.

(Also, yes, my NotNull does not admit custom AnyVal classes, but for the same reasons I don't think this is a likely use case.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option(x) violates that general contract by treating null in a non-type-parametric way

Is Try.apply equally flawed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but in a slightly different way.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could that be fixed with:

def apply[A <: NotNull](a: A | Null): Option[A] = ..

then your example would be:

def f[A <: NotNull](x: A, default: A): A = {
  val y = Option[A](x)
  y.getOrElse(default)
}
f[String | Null](null, "default") // fails compilation: `String | Null` is not `NotNull`

Copy link
Contributor

@olhotak olhotak Nov 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivating example is code like:

Option(somePossiblyNullThing) match {
  case Some(null) => ???
  case Some(s) => ???
  case None => ???
}

It would be nice to not have to include the first case.

More concretely, in minitest, there is the line:

Option(source.getMessage).filterNot(_.isEmpty)

This fails to compile because the Option() returns an Option[T|Null], so then the _ could be null, so you can't call .isEmpty on it. To get it to compile, we currently have to rewrite the above to:

Option(source.getMessage).filterNot(_.nn.isEmpty)

I think this defeats the main purpose of Option.apply, which is to get rid of the possibility of null by changing it to None.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems to be an inference issue, not a subtyping/type hierarchy issue. We could teach type inference that when it tries to infer A to unify A | Null with some T | Null, it should choose A = T instead of A = T | Null.

enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil)
}
def NullType: TypeRef = NullClass.typeRef
@tu 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 JavaNullAlias: TypeSymbol = {
assert(ctx.explicitNulls)
enterAliasType(tpnme.JavaNull, NullType)
}
def JavaNullAliasType: TypeRef = JavaNullAlias.typeRef

@tu lazy val ImplicitScrutineeTypeSym =
newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
Expand Down Expand Up @@ -508,12 +534,16 @@ class Definitions {
@tu lazy val BoxedNumberClass: ClassSymbol = ctx.requiredClass("java.lang.Number")
@tu lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException")
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
case List(pt) => (pt isRef StringClass)
case List(pt) =>
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
pt1 isRef StringClass
case _ => false
}).symbol.asTerm
@tu lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException")
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
case List(pt) => (pt isRef StringClass)
case List(pt) =>
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
pt1 isRef StringClass
case _ => false
}).symbol.asTerm

Expand Down Expand Up @@ -783,10 +813,29 @@ class Definitions {
@tu lazy val InfixAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.infix")
@tu lazy val AlphaAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.alpha")

// A list of annotations that are commonly used to indicate that a field/method argument or return
// type is not null. These annotations are used by the nullification logic in JavaNullInterop to
// improve the precision of type nullification.
// We don't require that any of these annotations be present in the class path, but we want to
// create Symbols for the ones that are present, so they can be checked during nullification.
@tu lazy val NotNullAnnots: List[ClassSymbol] = ctx.getClassesIfDefined(
"javax.annotation.Nonnull" ::
"edu.umd.cs.findbugs.annotations.NonNull" ::
"androidx.annotation.NonNull" ::
"android.support.annotation.NonNull" ::
"android.annotation.NonNull" ::
"com.android.annotations.NonNull" ::
"org.eclipse.jdt.annotation.NonNull" ::
"org.checkerframework.checker.nullness.qual.NonNull" ::
"org.checkerframework.checker.nullness.compatqual.NonNullDecl" ::
"org.jetbrains.annotations.NotNull" ::
"lombok.NonNull" ::
"io.reactivex.annotations.NonNull" :: Nil map PreNamedString)

// convenient one-parameter method types
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp)
def methOfAnyRef(tp: Type): MethodType = MethodType(List(ObjectType), tp)
def methOfAnyRefOrNull(tp: Type): MethodType = MethodType(List(ObjectType.maybeNullable), tp)

// Derived types

Expand Down Expand Up @@ -947,8 +996,16 @@ class Definitions {
name.drop(prefix.length).forall(_.isDigit))

def isBottomClass(cls: Symbol): Boolean =
cls == NothingClass || cls == NullClass
if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass
else isBottomClassAfterErasure(cls)

def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass

def isBottomType(tp: Type): Boolean =
if (ctx.explicitNulls && !ctx.phase.erasedTypes) tp.derivesFrom(NothingClass)
else isBottomTypeAfterErasure(tp)

def isBottomTypeAfterErasure(tp: Type): Boolean =
tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)

/** Is a function class.
Expand Down Expand Up @@ -1292,18 +1349,22 @@ class Definitions {
// ----- Initialization ---------------------------------------------------

/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = List(
AnyClass,
AnyRefAlias,
AnyKindClass,
andType,
orType,
RepeatedParamClass,
ByNameParamClass2x,
AnyValClass,
NullClass,
NothingClass,
SingletonClass)
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
val synth = List(
AnyClass,
AnyRefAlias,
AnyKindClass,
andType,
orType,
RepeatedParamClass,
ByNameParamClass2x,
AnyValClass,
NullClass,
NothingClass,
SingletonClass)

if (ctx.explicitNulls) synth :+ JavaNullAlias else synth
}

@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
EmptyPackageVal,
Expand Down
8 changes: 6 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Flags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,12 @@ object Flags {
* is completed)
*/
val AfterLoadFlags: FlagSet = commonFlags(
FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined)

FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined,
// We would like to add JavaEnumValue to this set so that we can correctly
// detect it in JavaNullInterop. However, JavaEnumValue is not initialized at this
// point, so we just make sure that all the "primitive" flags contained in JavaEnumValue
// are mentioned here as well.
Enum, StableRealizable)

/** A value that's unstable unless complemented with a Stable flag */
val UnstableValueFlags: FlagSet = Mutable | Method
Expand Down
Loading