Skip to content

Commit 241f8d7

Browse files
committed
Add an explicit nulls mode to Dotty
This commit adds a `-Yexplicit-nulls` experimental flag which, if enabled, modifies the type system so that reference types are non-nullable. Nullability is then expressed explicitly via type unions (e.g. `val s: String|Null = null`). At a high level, the changes to the compiler are: * A new type hierarchy for `Null`, which is now a subtype of `Any` directly, as opposed to being a subtype of _every_ refernce type. * A "translation layer" from Java types to Scala types so that Java interop can happen soundly yet conveniently. * An implementation of flow-typing so we can support a more natural style of programming with explicit nulls.
1 parent 655fbfe commit 241f8d7

File tree

109 files changed

+3223
-133
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+3223
-133
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ class ScalaSettings extends Settings.SettingGroup {
156156

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

160161
/** Area-specific debug output */
161162
val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.")

compiler/src/dotty/tools/dotc/core/Contexts.scala

+14-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import xsbti.AnalysisCallback
3737
import plugins._
3838
import java.util.concurrent.atomic.AtomicInteger
3939

40+
import dotty.tools.dotc.core.FlowTyper.FlowFacts
41+
4042
object Contexts {
4143

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

5255
/** A context is passed basically everywhere in dotc.
5356
* This is convenient but carries the risk of captured contexts in
@@ -143,6 +146,9 @@ object Contexts {
143146
protected def gadt_=(gadt: GadtConstraint): Unit = _gadt = gadt
144147
final def gadt: GadtConstraint = _gadt
145148

149+
/** The terms currently known to be non-null (in spite of their declared type) */
150+
def flowFacts: FlowFacts = store(flowFactsLoc)
151+
146152
/** The history of implicit searches that are currently active */
147153
private[this] var _searchHistory: SearchHistory = null
148154
protected def searchHistory_= (searchHistory: SearchHistory): Unit = _searchHistory = searchHistory
@@ -420,6 +426,9 @@ object Contexts {
420426
def useColors: Boolean =
421427
base.settings.color.value == "always"
422428

429+
/** Is the explicit nulls option set? */
430+
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
431+
423432
protected def init(outer: Context, origin: Context): this.type = {
424433
util.Stats.record("Context.fresh")
425434
_outer = outer
@@ -536,6 +545,10 @@ object Contexts {
536545
def setImportInfo(importInfo: ImportInfo): this.type = { this.importInfo = importInfo; this }
537546
def setGadt(gadt: GadtConstraint): this.type = { this.gadt = gadt; this }
538547
def setFreshGADTBounds: this.type = setGadt(gadt.fresh)
548+
def addFlowFacts(facts: FlowFacts): this.type = {
549+
assert(settings.YexplicitNulls.value)
550+
updateStore(flowFactsLoc, store(flowFactsLoc) ++ facts)
551+
}
539552
def setSearchHistory(searchHistory: SearchHistory): this.type = { this.searchHistory = searchHistory; this }
540553
def setSource(source: SourceFile): this.type = { this.source = source; this }
541554
def setTypeComparerFn(tcfn: Context => TypeComparer): this.type = { this.typeComparer = tcfn(this); this }

compiler/src/dotty/tools/dotc/core/Definitions.scala

+85-26
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package core
44

55
import scala.annotation.threadUnsafe
66
import Types._, Contexts._, Symbols._, SymDenotations._, StdNames._, Names._
7-
import Flags._, Scopes._, Decorators._, NameOps._, Periods._
7+
import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._
88
import unpickleScala2.Scala2Unpickler.ensureConstructor
99
import scala.collection.mutable
1010
import collection.mutable
@@ -293,7 +293,7 @@ class Definitions {
293293
@threadUnsafe lazy val ObjectClass: ClassSymbol = {
294294
val cls = ctx.requiredClass("java.lang.Object")
295295
assert(!cls.isCompleted, "race for completing java.lang.Object")
296-
cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: Nil, newScope)
296+
cls.info = ClassInfo(cls.owner.thisType, cls, AnyType :: Nil, newScope)
297297
cls.setFlag(NoInits)
298298

299299
// The companion object doesn't really exist, `NoType` is the general
@@ -309,8 +309,18 @@ class Definitions {
309309
@threadUnsafe lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType)
310310
def AnyRefType: TypeRef = AnyRefAlias.typeRef
311311

312-
@threadUnsafe lazy val Object_eq: TermSymbol = enterMethod(ObjectClass, nme.eq, methOfAnyRef(BooleanType), Final)
313-
@threadUnsafe lazy val Object_ne: TermSymbol = enterMethod(ObjectClass, nme.ne, methOfAnyRef(BooleanType), Final)
312+
@threadUnsafe lazy val Object_eq: TermSymbol = {
313+
// If explicit nulls is enabled, then we want to allow `(x: String).eq(null)`, so we need
314+
// to adjust the signature of `eq` accordingly.
315+
val tpe = if (ctx.explicitNulls) methOfAnyRefOrNull(BooleanType) else methOfAnyRef(BooleanType)
316+
enterMethod(ObjectClass, nme.eq, tpe, Final)
317+
}
318+
@threadUnsafe lazy val Object_ne: TermSymbol = {
319+
// If explicit nulls is enabled, then we want to allow `(x: String).ne(null)`, so we need
320+
// to adjust the signature of `ne` accordingly.
321+
val tpe = if (ctx.explicitNulls) methOfAnyRefOrNull(BooleanType) else methOfAnyRef(BooleanType)
322+
enterMethod(ObjectClass, nme.ne, tpe, Final)
323+
}
314324
@threadUnsafe lazy val Object_synchronized: TermSymbol = enterPolyMethod(ObjectClass, nme.synchronized_, 1,
315325
pt => MethodType(List(pt.paramRefs(0)), pt.paramRefs(0)), Final)
316326
@threadUnsafe lazy val Object_clone: TermSymbol = enterMethod(ObjectClass, nme.clone_, MethodType(Nil, ObjectType), Protected)
@@ -344,18 +354,42 @@ class Definitions {
344354
pt => MethodType(List(FunctionOf(Nil, pt.paramRefs(0))), pt.paramRefs(0)))
345355

346356
/** Method representing a throw */
347-
@threadUnsafe lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw,
348-
MethodType(List(ThrowableType), NothingType))
357+
@threadUnsafe lazy val throwMethod: TermSymbol = {
358+
val argTpe = if (ctx.explicitNulls) OrType(ThrowableType, NullType) else ThrowableType
359+
enterMethod(OpsPackageClass, nme.THROWkw, MethodType(List(argTpe), NothingType))
360+
}
349361

350362
@threadUnsafe lazy val NothingClass: ClassSymbol = enterCompleteClassSymbol(
351363
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef))
352364
def NothingType: TypeRef = NothingClass.typeRef
353365
@threadUnsafe lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing")
354-
@threadUnsafe lazy val NullClass: ClassSymbol = enterCompleteClassSymbol(
355-
ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef))
366+
367+
@threadUnsafe lazy val NullClass: ClassSymbol = {
368+
val parents = List(if (ctx.explicitNulls) AnyType else ObjectType)
369+
enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parents)
370+
}
356371
def NullType: TypeRef = NullClass.typeRef
357372
@threadUnsafe lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null")
358373

374+
/** An alias for null values that originate in Java code.
375+
* This type gets special treatment in the Typer. Specifically, `JavaNull` can be selected through:
376+
* e.g.
377+
* ```
378+
* // x: String|Null
379+
* x.length // error: `Null` has no `length` field
380+
* // x2: String|JavaNull
381+
* x2.length // allowed by the Typer, but unsound (might throw NPE)
382+
* ```
383+
*/
384+
lazy val JavaNullAlias: TypeSymbol = {
385+
assert(ctx.explicitNulls)
386+
enterAliasType(tpnme.JavaNull, NullType)
387+
}
388+
def JavaNullAliasType: TypeRef = {
389+
assert(ctx.explicitNulls)
390+
JavaNullAlias.typeRef
391+
}
392+
359393
@threadUnsafe lazy val ImplicitScrutineeTypeSym =
360394
newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
361395
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -591,12 +625,16 @@ class Definitions {
591625
@threadUnsafe lazy val BoxedNumberClass: ClassSymbol = ctx.requiredClass("java.lang.Number")
592626
@threadUnsafe lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException")
593627
@threadUnsafe lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
594-
case List(pt) => (pt isRef StringClass)
628+
case List(pt) =>
629+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
630+
pt1 isRef StringClass
595631
case _ => false
596632
}).symbol.asTerm
597633
@threadUnsafe lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException")
598634
@threadUnsafe lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
599-
case List(pt) => (pt isRef StringClass)
635+
case List(pt) =>
636+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
637+
pt1 isRef StringClass
600638
case _ => false
601639
}).symbol.asTerm
602640

@@ -967,6 +1005,7 @@ class Definitions {
9671005
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
9681006
def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp)
9691007
def methOfAnyRef(tp: Type): MethodType = MethodType(List(ObjectType), tp)
1008+
def methOfAnyRefOrNull(tp: Type): MethodType = MethodType(List(OrType(ObjectType, NullType)), tp)
9701009

9711010
// Derived types
9721011

@@ -1128,10 +1167,23 @@ class Definitions {
11281167
name.length > prefix.length &&
11291168
name.drop(prefix.length).forall(_.isDigit))
11301169

1131-
def isBottomClass(cls: Symbol): Boolean =
1170+
def isBottomClass(cls: Symbol): Boolean = {
1171+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass
1172+
else isBottomClassAfterErasure(cls)
1173+
}
1174+
1175+
def isBottomClassAfterErasure(cls: Symbol): Boolean = {
11321176
cls == NothingClass || cls == NullClass
1133-
def isBottomType(tp: Type): Boolean =
1177+
}
1178+
1179+
def isBottomType(tp: Type): Boolean = {
1180+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) tp.derivesFrom(NothingClass)
1181+
else isBottomTypeAfterErasure(tp)
1182+
}
1183+
1184+
def isBottomTypeAfterErasure(tp: Type): Boolean = {
11341185
tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)
1186+
}
11351187

11361188
/** Is a function class.
11371189
* - FunctionXXL
@@ -1225,9 +1277,12 @@ class Definitions {
12251277
() => ScalaPackageVal.termRef
12261278
)
12271279

1228-
val PredefImportFns: List[() => TermRef] = List[() => TermRef](
1280+
lazy val PredefImportFns: List[() => TermRef] = List[() => TermRef](
12291281
() => ScalaPredefModuleRef,
1230-
() => DottyPredefModuleRef
1282+
() => DottyPredefModuleRef,
1283+
// TODO(abeln): is this in the right place?
1284+
// And is it ok to import this unconditionally?
1285+
() => ctx.requiredModuleRef("scala.ExplicitNullsOps")
12311286
)
12321287

12331288
@threadUnsafe lazy val RootImportFns: List[() => TermRef] =
@@ -1475,18 +1530,22 @@ class Definitions {
14751530
// ----- Initialization ---------------------------------------------------
14761531

14771532
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1478-
@threadUnsafe lazy val syntheticScalaClasses: List[TypeSymbol] = List(
1479-
AnyClass,
1480-
AnyRefAlias,
1481-
AnyKindClass,
1482-
andType,
1483-
orType,
1484-
RepeatedParamClass,
1485-
ByNameParamClass2x,
1486-
AnyValClass,
1487-
NullClass,
1488-
NothingClass,
1489-
SingletonClass)
1533+
@threadUnsafe lazy val syntheticScalaClasses: List[TypeSymbol] = {
1534+
val synth = List(
1535+
AnyClass,
1536+
AnyRefAlias,
1537+
AnyKindClass,
1538+
andType,
1539+
orType,
1540+
RepeatedParamClass,
1541+
ByNameParamClass2x,
1542+
AnyValClass,
1543+
NullClass,
1544+
NothingClass,
1545+
SingletonClass)
1546+
1547+
if (ctx.explicitNulls) synth :+ JavaNullAlias else synth
1548+
}
14901549

14911550
@threadUnsafe lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
14921551
EmptyPackageVal,

compiler/src/dotty/tools/dotc/core/Flags.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,6 @@ object Flags {
358358
/** An inferable (`given`) parameter */
359359
final val Given = commonFlag(29, "given")
360360

361-
/** Symbol is defined by a Java class */
362-
final val JavaDefined: FlagSet = commonFlag(30, "<java>")
363-
364361
/** Symbol is implemented as a Java static */
365362
final val JavaStatic: FlagSet = commonFlag(31, "<static>")
366363
final val JavaStaticTerm: FlagSet = JavaStatic.toTermFlags
@@ -397,6 +394,12 @@ object Flags {
397394
/** Symbol is an enum class or enum case (if used with case) */
398395
final val Enum: FlagSet = commonFlag(40, "<enum>")
399396

397+
/** Symbol is defined by a Java class */
398+
final val JavaDefined: FlagSet = commonFlag(30, "<java>")
399+
400+
/** A Java enum value */
401+
final val JavaEnumValue: FlagConjunction = allOf(StableRealizable, JavaStatic, JavaDefined, Enum)
402+
400403
/** An export forwarder */
401404
final val Exported: FlagSet = commonFlag(41, "exported")
402405

@@ -488,7 +491,7 @@ object Flags {
488491
final val FromStartFlags: FlagSet =
489492
Module | Package | Deferred | Method.toCommonFlags | Case |
490493
HigherKinded.toCommonFlags | Param | ParamAccessor.toCommonFlags |
491-
Scala2ExistentialCommon | MutableOrOpaque | Touched | JavaStatic |
494+
Scala2ExistentialCommon | MutableOrOpaque | Touched | JavaStatic | JavaDefined | JavaEnumValue.toCommonFlags |
492495
CovariantOrOuter | ContravariantOrLabel | CaseAccessor.toCommonFlags |
493496
Extension.toCommonFlags | NonMember | Implicit | Given | Implied | Permanent | Synthetic |
494497
SuperAccessorOrScala2x | Inline
@@ -690,9 +693,6 @@ object Flags {
690693
/** A Java enum trait */
691694
final val JavaEnumTrait: FlagConjunction = allOf(JavaDefined, Enum)
692695

693-
/** A Java enum value */
694-
final val JavaEnumValue: FlagConjunction = allOf(StableRealizable, JavaStatic, JavaDefined, Enum)
695-
696696
/** An enum value */
697697
final val EnumValue: FlagConjunction = allOf(StableRealizable, JavaStatic, Enum)
698698

0 commit comments

Comments
 (0)