Skip to content

Commit 2b15184

Browse files
authored
Merge pull request #7556 from dotty-staging/add-flow-analysis-without-notnull
Nullability Analysis without NotNull
2 parents 73a90dd + 016f471 commit 2b15184

29 files changed

+668
-79
lines changed

compiler/src/dotty/tools/dotc/CompilationUnit.scala

+12
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import util.SourceFile
55
import ast.{tpd, untpd}
66
import tpd.{Tree, TreeTraverser}
77
import typer.PrepareInlineable.InlineAccessors
8+
import typer.Nullables
89
import dotty.tools.dotc.core.Contexts.Context
910
import dotty.tools.dotc.core.SymDenotations.ClassDenotation
1011
import dotty.tools.dotc.core.Symbols._
1112
import dotty.tools.dotc.transform.SymUtils._
1213
import util.{NoSource, SourceFile}
14+
import util.Spans.Span
1315
import core.Decorators._
1416

1517
class CompilationUnit protected (val source: SourceFile) {
@@ -42,6 +44,16 @@ class CompilationUnit protected (val source: SourceFile) {
4244
suspended = true
4345
ctx.run.suspendedUnits += this
4446
throw CompilationUnit.SuspendException()
47+
48+
private var myAssignmentSpans: Map[Int, List[Span]] = null
49+
50+
/** A map from (name-) offsets of all local variables in this compilation unit
51+
* that can be tracked for being not null to the list of spans of assignments
52+
* to these variables.
53+
*/
54+
def assignmentSpans(given Context): Map[Int, List[Span]] =
55+
if myAssignmentSpans == null then myAssignmentSpans = Nullables.assignmentSpans
56+
myAssignmentSpans
4557
}
4658

4759
object CompilationUnit {

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

+9-1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] =>
8888
/** If this is a block, its expression part */
8989
def stripBlock(tree: Tree): Tree = unsplice(tree) match {
9090
case Block(_, expr) => stripBlock(expr)
91+
case Inlined(_, _, expr) => stripBlock(expr)
92+
case _ => tree
93+
}
94+
95+
def stripInlined(tree: Tree): Tree = unsplice(tree) match {
96+
case Inlined(_, _, expr) => stripInlined(expr)
9197
case _ => tree
9298
}
9399

@@ -391,7 +397,9 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
391397
if (fn.symbol.is(Erased) || fn.symbol == defn.InternalQuoted_typeQuote) Pure else exprPurity(fn)
392398
case Apply(fn, args) =>
393399
def isKnownPureOp(sym: Symbol) =
394-
sym.owner.isPrimitiveValueClass || sym.owner == defn.StringClass
400+
sym.owner.isPrimitiveValueClass
401+
|| sym.owner == defn.StringClass
402+
|| defn.pureMethods.contains(sym)
395403
if (tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol) // A constant expression with pure arguments is pure.
396404
|| (fn.symbol.isStableMember && !fn.symbol.is(Lazy))
397405
|| fn.symbol.isPrimaryConstructor && fn.symbol.owner.isNoInitsClass) // TODO: include in isStable?

compiler/src/dotty/tools/dotc/ast/tpd.scala

+11-3
Original file line numberDiff line numberDiff line change
@@ -711,11 +711,19 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
711711

712712
class TimeTravellingTreeCopier extends TypedTreeCopier {
713713
override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(implicit ctx: Context): Apply =
714-
ta.assignType(untpdCpy.Apply(tree)(fun, args), fun, args)
714+
tree match
715+
case tree: Apply
716+
if (tree.fun eq fun) && (tree.args eq args)
717+
&& tree.tpe.isInstanceOf[ConstantType]
718+
&& isPureExpr(tree) => tree
719+
case _ =>
720+
ta.assignType(untpdCpy.Apply(tree)(fun, args), fun, args)
715721
// Note: Reassigning the original type if `fun` and `args` have the same types as before
716-
// does not work here: The computed type depends on the widened function type, not
717-
// the function type itself. A treetransform may keep the function type the
722+
// does not work here in general: The computed type depends on the widened function type, not
723+
// the function type itself. A tree transform may keep the function type the
718724
// same but its widened type might change.
725+
// However, we keep constant types of pure expressions. This uses the underlying assumptions
726+
// that pure functions yielding a constant will not change in later phases.
719727

720728
override def TypeApply(tree: Tree)(fun: Tree, args: List[Tree])(implicit ctx: Context): TypeApply =
721729
ta.assignType(untpdCpy.TypeApply(tree)(fun, args), fun, args)

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

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

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

166167
/** Area-specific debug output */
167168
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

+25-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import ast.Trees._
1515
import ast.untpd
1616
import Flags.GivenOrImplicit
1717
import util.{FreshNameCreator, NoSource, SimpleIdentityMap, SourceFile}
18-
import typer.{Implicits, ImportInfo, Inliner, NamerContextOps, SearchHistory, SearchRoot, TypeAssigner, Typer}
18+
import typer.{Implicits, ImportInfo, Inliner, NamerContextOps, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables}
19+
import Nullables.{NotNullInfo, given}
1920
import Implicits.ContextualImplicits
2021
import config.Settings._
2122
import config.Config
@@ -47,7 +48,11 @@ object Contexts {
4748
private val (compilationUnitLoc, store6) = store5.newLocation[CompilationUnit]()
4849
private val (runLoc, store7) = store6.newLocation[Run]()
4950
private val (profilerLoc, store8) = store7.newLocation[Profiler]()
50-
private val initialStore = store8
51+
private val (notNullInfosLoc, store9) = store8.newLocation[List[NotNullInfo]]()
52+
private val initialStore = store9
53+
54+
/** The current context */
55+
def curCtx(given ctx: Context): Context = ctx
5156

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

215+
/** The paths currently known to be not null */
216+
def notNullInfos = store(notNullInfosLoc)
217+
210218
/** The new implicit references that are introduced by this scope */
211219
protected var implicitsCache: ContextualImplicits = null
212220
def implicits: ContextualImplicits = {
@@ -556,6 +564,7 @@ object Contexts {
556564
def setRun(run: Run): this.type = updateStore(runLoc, run)
557565
def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler)
558566
def setFreshNames(freshNames: FreshNameCreator): this.type = updateStore(freshNamesLoc, freshNames)
567+
def setNotNullInfos(notNullInfos: List[NotNullInfo]): this.type = updateStore(notNullInfosLoc, notNullInfos)
559568

560569
def setProperty[T](key: Key[T], value: T): this.type =
561570
setMoreProperties(moreProperties.updated(key, value))
@@ -587,6 +596,17 @@ object Contexts {
587596
def setDebug: this.type = setSetting(base.settings.Ydebug, true)
588597
}
589598

599+
given (c: Context)
600+
def addNotNullInfo(info: NotNullInfo) =
601+
c.withNotNullInfos(c.notNullInfos.extendWith(info))
602+
603+
def addNotNullRefs(refs: Set[TermRef]) =
604+
c.addNotNullInfo(NotNullInfo(refs, Set()))
605+
606+
def withNotNullInfos(infos: List[NotNullInfo]): Context =
607+
if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos)
608+
609+
// TODO: Fix issue when converting ModeChanges and FreshModeChanges to extension givens
590610
implicit class ModeChanges(val c: Context) extends AnyVal {
591611
final def withModeBits(mode: Mode): Context =
592612
if (mode != c.mode) c.fresh.setMode(mode) else c
@@ -615,7 +635,9 @@ object Contexts {
615635
typeAssigner = TypeAssigner
616636
moreProperties = Map.empty
617637
source = NoSource
618-
store = initialStore.updated(settingsStateLoc, settingsGroup.defaultState)
638+
store = initialStore
639+
.updated(settingsStateLoc, settingsGroup.defaultState)
640+
.updated(notNullInfosLoc, Nil)
619641
typeComparer = new TypeComparer(this)
620642
searchHistory = new SearchRoot
621643
gadt = EmptyGadtConstraint

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

+4
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ class Definitions {
310310
def ObjectMethods: List[TermSymbol] = List(Object_eq, Object_ne, Object_synchronized, Object_clone,
311311
Object_finalize, Object_notify, Object_notifyAll, Object_wait, Object_waitL, Object_waitLI)
312312

313+
/** Methods in Object and Any that do not have a side effect */
314+
@tu lazy val pureMethods: List[TermSymbol] = List(Any_==, Any_!=, Any_equals, Any_hashCode,
315+
Any_toString, Any_##, Any_getClass, Any_isInstanceOf, Any_typeTest, Object_eq, Object_ne)
316+
313317
@tu lazy val AnyKindClass: ClassSymbol = {
314318
val cls = ctx.newCompleteClassSymbol(ScalaPackageClass, tpnme.AnyKind, AbstractFinal | Permanent, Nil)
315319
if (!ctx.settings.YnoKindPolymorphism.value)

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

+2
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ object StdNames {
195195
final val ExprApi: N = "ExprApi"
196196
final val Mirror: N = "Mirror"
197197
final val Nothing: N = "Nothing"
198+
final val NotNull: N = "NotNull"
198199
final val Null: N = "Null"
199200
final val Object: N = "Object"
200201
final val Product: N = "Product"
@@ -261,6 +262,7 @@ object StdNames {
261262
val MIRROR_PREFIX: N = "$m."
262263
val MIRROR_SHORT: N = "$m"
263264
val MIRROR_UNTYPED: N = "$m$untyped"
265+
val NOT_NULL: N = "$nn"
264266
val REIFY_FREE_PREFIX: N = "free$"
265267
val REIFY_FREE_THIS_SUFFIX: N = "$this"
266268
val REIFY_FREE_VALUE_SUFFIX: N = "$value"

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1838,7 +1838,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
18381838
else if (!tp2.exists) tp1
18391839
else tp.derivedAndType(tp1, tp2)
18401840

1841-
/** If some (&-operand of) this type is a supertype of `sub` replace it with `NoType`.
1841+
/** If some (&-operand of) `tp` is a supertype of `sub` replace it with `NoType`.
18421842
*/
18431843
private def dropIfSuper(tp: Type, sub: Type): Type =
18441844
if (isSubTypeWhenFrozen(sub, tp)) NoType

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
8181
// called which we override to set the `approximated` flag.
8282
range(defn.NothingType, pre)
8383
else pre
84-
else if ((pre.termSymbol is Package) && !(thiscls is Package))
84+
else if (pre.termSymbol.is(Package) && !thiscls.is(Package))
8585
toPrefix(pre.select(nme.PACKAGE), cls, thiscls)
8686
else
8787
toPrefix(pre.baseType(cls).normalizedPrefix, cls.owner, thiscls)

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

+27-3
Original file line numberDiff line numberDiff line change
@@ -1090,8 +1090,9 @@ object Types {
10901090
* instead of `ArrayBuffer[? >: Int | A <: Int & A]`
10911091
*/
10921092
def widenUnion(implicit ctx: Context): Type = widen match {
1093-
case OrType(tp1, tp2) =>
1094-
ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match {
1093+
case tp @ OrType(tp1, tp2) =>
1094+
if tp1.isNull || tp2.isNull then tp
1095+
else ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match {
10951096
case union: OrType => union.join
10961097
case res => res
10971098
}
@@ -1421,6 +1422,11 @@ object Types {
14211422
case _ => true
14221423
}
14231424

1425+
/** Is this (an alias of) the `scala.Null` type? */
1426+
final def isNull(given Context) =
1427+
isRef(defn.NullClass)
1428+
|| classSymbol.name == tpnme.Null // !!! temporary kludge for being able to test without the explicit nulls PR
1429+
14241430
/** The resultType of a LambdaType, or ExprType, the type itself for others */
14251431
def resultType(implicit ctx: Context): Type = this
14261432

@@ -2315,7 +2321,7 @@ object Types {
23152321
}
23162322

23172323
/** The singleton type for path prefix#myDesignator.
2318-
*/
2324+
*/
23192325
abstract case class TermRef(override val prefix: Type,
23202326
private var myDesignator: Designator)
23212327
extends NamedType with SingletonType with ImplicitRef {
@@ -2908,6 +2914,24 @@ object Types {
29082914
else apply(tp1, tp2)
29092915
}
29102916

2917+
/** An extractor for `T | Null` or `Null | T`, returning the `T` */
2918+
object OrNull with
2919+
private def stripNull(tp: Type)(given Context): Type = tp match
2920+
case tp @ OrType(tp1, tp2) =>
2921+
if tp1.isNull then tp2
2922+
else if tp2.isNull then tp1
2923+
else tp.derivedOrType(stripNull(tp1), stripNull(tp2))
2924+
case tp @ AndType(tp1, tp2) =>
2925+
tp.derivedAndType(stripNull(tp1), stripNull(tp2))
2926+
case _ =>
2927+
tp
2928+
def apply(tp: Type)(given Context) =
2929+
OrType(tp, defn.NullType)
2930+
def unapply(tp: Type)(given Context): Option[Type] =
2931+
val tp1 = stripNull(tp)
2932+
if tp1 ne tp then Some(tp1) else None
2933+
end OrNull
2934+
29112935
// ----- ExprType and LambdaTypes -----------------------------------
29122936

29132937
// Note: method types are cached whereas poly types are not. The reason

compiler/src/dotty/tools/dotc/interactive/Completion.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ object Completion {
207207
def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit =
208208
if (!qual.tpe.widenDealias.isBottomType) {
209209
addAccessibleMembers(qual.tpe)
210-
if (!mode.is(Mode.Import) && !qual.tpe.isRef(defn.NullClass))
210+
if (!mode.is(Mode.Import) && !qual.tpe.isNull)
211211
// Implicit conversions do not kick in when importing
212212
// and for `NullClass` they produce unapplicable completions (for unclear reasons)
213213
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState())

compiler/src/dotty/tools/dotc/transform/Erasure.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -740,11 +740,12 @@ object Erasure {
740740
override def typedAnnotated(tree: untpd.Annotated, pt: Type)(implicit ctx: Context): Tree =
741741
typed(tree.arg, pt)
742742

743-
override def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): List[Tree] = {
743+
override def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): (List[Tree], Context) = {
744744
val stats1 =
745745
if (takesBridges(ctx.owner)) new Bridges(ctx.owner.asClass, erasurePhase).add(stats)
746746
else stats
747-
super.typedStats(stats1, exprOwner).filter(!_.isEmpty)
747+
val (stats2, finalCtx) = super.typedStats(stats1, exprOwner)
748+
(stats2.filter(!_.isEmpty), finalCtx)
748749
}
749750

750751
override def adapt(tree: Tree, pt: Type, locked: TypeVars)(implicit ctx: Context): Tree =

compiler/src/dotty/tools/dotc/transform/FirstTransform.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase =>
160160
constToLiteral(tree)
161161

162162
override def transformIf(tree: If)(implicit ctx: Context): Tree =
163-
tree.cond match {
164-
case Literal(Constant(c: Boolean)) => if (c) tree.thenp else tree.elsep
163+
tree.cond.tpe match {
164+
case ConstantType(Constant(c: Boolean)) if isPureExpr(tree.cond) =>
165+
if (c) tree.thenp else tree.elsep
165166
case _ => tree
166167
}
167168

compiler/src/dotty/tools/dotc/transform/TreeChecker.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,9 @@ class TreeChecker extends Phase with SymTransformer {
434434
}
435435
}
436436

437-
override def typedCase(tree: untpd.CaseDef, selType: Type, pt: Type)(implicit ctx: Context): CaseDef =
437+
override def typedCase(tree: untpd.CaseDef, sel: Tree, selType: Type, pt: Type)(implicit ctx: Context): CaseDef =
438438
withPatSyms(tpd.patVars(tree.pat.asInstanceOf[tpd.Tree])) {
439-
super.typedCase(tree, selType, pt)
439+
super.typedCase(tree, sel, selType, pt)
440440
}
441441

442442
override def typedClosure(tree: untpd.Closure, pt: Type)(implicit ctx: Context): Tree = {
@@ -466,7 +466,7 @@ class TreeChecker extends Phase with SymTransformer {
466466
* is that we should be able to pull out an expression as an initializer
467467
* of a helper value without having to do a change owner traversal of the expression.
468468
*/
469-
override def typedStats(trees: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): List[Tree] = {
469+
override def typedStats(trees: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): (List[Tree], Context) = {
470470
for (tree <- trees) tree match {
471471
case tree: untpd.DefTree => checkOwner(tree)
472472
case _: untpd.Thicket => assert(false, i"unexpanded thicket $tree in statement sequence $trees%\n%")

compiler/src/dotty/tools/dotc/typer/Applications.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import NameKinds.DefaultGetterName
2222
import ProtoTypes._
2323
import Inferencing._
2424
import transform.TypeUtils._
25+
import Nullables.given
2526

2627
import collection.mutable
2728
import config.Printers.{overload, typr, unapp}
@@ -864,8 +865,9 @@ trait Applications extends Compatibility {
864865
if (proto.allArgTypesAreCurrent())
865866
new ApplyToTyped(tree, fun1, funRef, proto.unforcedTypedArgs, pt)
866867
else
867-
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(argCtx(tree))
868-
convertNewGenericArray(app.result)
868+
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(
869+
given fun1.nullableInArgContext(given argCtx(tree)))
870+
convertNewGenericArray(app.result).computeNullable()
869871
case _ =>
870872
handleUnexpectedFunType(tree, fun1)
871873
}

compiler/src/dotty/tools/dotc/typer/ConstFold.scala

+7-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Constants._
1111
import Names._
1212
import StdNames._
1313
import Contexts._
14+
import Nullables.{CompareNull, TrackedRef}
1415

1516
object ConstFold {
1617

@@ -19,15 +20,17 @@ object ConstFold {
1920
/** If tree is a constant operation, replace with result. */
2021
def apply[T <: Tree](tree: T)(implicit ctx: Context): T = finish(tree) {
2122
tree match {
23+
case CompareNull(TrackedRef(ref), testEqual)
24+
if ctx.settings.YexplicitNulls.value && ctx.notNullInfos.impliesNotNull(ref) =>
25+
// TODO maybe drop once we have general Nullability?
26+
Constant(!testEqual)
2227
case Apply(Select(xt, op), yt :: Nil) =>
23-
xt.tpe.widenTermRefExpr.normalized match {
28+
xt.tpe.widenTermRefExpr.normalized match
2429
case ConstantType(x) =>
25-
yt.tpe.widenTermRefExpr match {
30+
yt.tpe.widenTermRefExpr match
2631
case ConstantType(y) => foldBinop(op, x, y)
2732
case _ => null
28-
}
2933
case _ => null
30-
}
3134
case Select(xt, op) =>
3235
xt.tpe.widenTermRefExpr match {
3336
case ConstantType(x) => foldUnop(op, x)

compiler/src/dotty/tools/dotc/typer/Docstrings.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object Docstrings {
3333
expandComment(sym).map { expanded =>
3434
val typedUsecases = expanded.usecases.map { usecase =>
3535
ctx.typer.enterSymbol(ctx.typer.createSymbol(usecase.untpdCode))
36-
ctx.typer.typedStats(usecase.untpdCode :: Nil, owner) match {
36+
ctx.typer.typedStats(usecase.untpdCode :: Nil, owner)._1 match {
3737
case List(df: tpd.DefDef) =>
3838
usecase.typed(df)
3939
case _ =>

compiler/src/dotty/tools/dotc/typer/Inliner.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ErrorReporting.errorTree
2424
import dotty.tools.dotc.tastyreflect.ReflectionImpl
2525
import dotty.tools.dotc.util.{SimpleIdentityMap, SimpleIdentitySet, SourceFile, SourcePosition}
2626
import dotty.tools.dotc.parsing.Parsers.Parser
27+
import Nullables.given
2728

2829
import collection.mutable
2930
import reporting.trace
@@ -1064,10 +1065,10 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) {
10641065
errorTree(tree, em"""cannot reduce inline if
10651066
| its condition ${tree.cond}
10661067
| is not a constant value""")
1067-
else {
1068+
else
1069+
cond1.computeNullableDeeply()
10681070
val if1 = untpd.cpy.If(tree)(cond = untpd.TypedSplice(cond1))
10691071
super.typedIf(if1, pt)
1070-
}
10711072
}
10721073

10731074
override def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context): Tree =

0 commit comments

Comments
 (0)