Skip to content

Commit 575df47

Browse files
committed
Allow retracting a not null status
This is needed to support mutable variables. It required a fundamental change in data structures. Now, we keep two sets where before we kept one: The set of references known to be not null, and the set of references that are retracted, so that they can be null again.
1 parent 076091f commit 575df47

File tree

5 files changed

+112
-71
lines changed

5 files changed

+112
-71
lines changed

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import ast.untpd
1616
import Flags.GivenOrImplicit
1717
import util.{FreshNameCreator, NoSource, SimpleIdentityMap, SourceFile}
1818
import typer.{Implicits, ImportInfo, Inliner, NamerContextOps, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables}
19-
import Nullables.given
19+
import Nullables.{NotNullInfo, given}
2020
import Implicits.ContextualImplicits
2121
import config.Settings._
2222
import config.Config
@@ -48,7 +48,7 @@ object Contexts {
4848
private val (compilationUnitLoc, store6) = store5.newLocation[CompilationUnit]()
4949
private val (runLoc, store7) = store6.newLocation[Run]()
5050
private val (profilerLoc, store8) = store7.newLocation[Profiler]()
51-
private val (notNullRefsLoc, store9) = store8.newLocation[List[Nullables.Excluded]]()
51+
private val (notNullInfosLoc, store9) = store8.newLocation[List[NotNullInfo]]()
5252
private val initialStore = store9
5353

5454
/** The current context */
@@ -213,7 +213,7 @@ object Contexts {
213213
def profiler: Profiler = store(profilerLoc)
214214

215215
/** The paths currently known to be not null */
216-
def notNullRefs = store(notNullRefsLoc)
216+
def notNullInfos = store(notNullInfosLoc)
217217

218218
/** The new implicit references that are introduced by this scope */
219219
protected var implicitsCache: ContextualImplicits = null
@@ -564,7 +564,7 @@ object Contexts {
564564
def setRun(run: Run): this.type = updateStore(runLoc, run)
565565
def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler)
566566
def setFreshNames(freshNames: FreshNameCreator): this.type = updateStore(freshNamesLoc, freshNames)
567-
def setNotNullRefs(notNullRefs: List[Nullables.Excluded]): this.type = updateStore(notNullRefsLoc, notNullRefs)
567+
def setNotNullInfos(notNullInfos: List[NotNullInfo]): this.type = updateStore(notNullInfosLoc, notNullInfos)
568568

569569
def setProperty[T](key: Key[T], value: T): this.type =
570570
setMoreProperties(moreProperties.updated(key, value))
@@ -597,12 +597,14 @@ object Contexts {
597597
}
598598

599599
given (c: Context)
600-
def addExcluded(refs: Nullables.Excluded) =
601-
if c.notNullRefs.containsAll(refs) then c
602-
else c.fresh.setNotNullRefs(refs :: c.notNullRefs)
600+
def addNotNullInfo(info: NotNullInfo) =
601+
c.withNotNullInfos(c.notNullInfos.extendWith(info))
603602

604-
def withNotNullRefs(nnrefs: List[Nullables.Excluded]): Context =
605-
if c.notNullRefs eq nnrefs then c else c.fresh.setNotNullRefs(nnrefs)
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)
606608

607609
// TODO: Fix issue when converting ModeChanges and FreshModeChanges to extension givens
608610
implicit class ModeChanges(val c: Context) extends AnyVal {
@@ -635,7 +637,7 @@ object Contexts {
635637
source = NoSource
636638
store = initialStore
637639
.updated(settingsStateLoc, settingsGroup.defaultState)
638-
.updated(notNullRefsLoc, Nil)
640+
.updated(notNullInfosLoc, Nil)
639641
typeComparer = new TypeComparer(this)
640642
searchHistory = new SearchRoot
641643
gadt = EmptyGadtConstraint

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ trait Applications extends Compatibility {
866866
new ApplyToTyped(tree, fun1, funRef, proto.unforcedTypedArgs, pt)
867867
else
868868
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(
869-
fun1.nullableInArgContext(given argCtx(tree)))
869+
given fun1.nullableInArgContext(given argCtx(tree)))
870870
convertNewGenericArray(app.result).computeNullable()
871871
case _ =>
872872
handleUnexpectedFunType(tree, fun1)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ object ConstFold {
2020
/** If tree is a constant operation, replace with result. */
2121
def apply[T <: Tree](tree: T)(implicit ctx: Context): T = finish(tree) {
2222
tree match {
23-
case CompareNull(TrackedRef(ref), testEqual) if ctx.notNullRefs.containsRef(ref) =>
23+
case CompareNull(TrackedRef(ref), testEqual) if ctx.notNullInfos.containsRef(ref) =>
2424
// TODO maybe drop once we have general Nullability?
2525
Constant(!testEqual)
2626
case Apply(Select(xt, op), yt :: Nil) =>

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

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,58 @@ package typer
44

55
import core._
66
import Types._, Contexts._, Symbols._, Decorators._, Constants._
7-
import annotation.tailrec
7+
import annotation.{tailrec, infix}
88
import util.Property
99

1010
/** Operations for implementing a flow analysis for nullability */
1111
object Nullables with
1212
import ast.tpd._
1313

14-
/** A set of paths that are known to be not null */
15-
type Excluded = Set[TermRef]
14+
/** A set of val or var references that are known to be not null, plus a set of
15+
* variable references that are not known (anymore) to be null
16+
*/
17+
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef])
18+
assert((asserted & retracted).isEmpty)
19+
20+
def isEmpty = this eq noNotNulls
21+
22+
/** The sequential combination with another not-null info */
23+
@infix def seq(that: NotNullInfo): NotNullInfo =
24+
if this.isEmpty then that
25+
else if that.isEmpty then this
26+
else NotNullInfo(
27+
this.asserted.union(that.asserted).diff(that.retracted),
28+
this.retracted.union(that.retracted).diff(that.asserted))
29+
30+
object NotNullInfo with
31+
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
32+
if asserted.isEmpty && retracted.isEmpty then noNotNulls
33+
else new NotNullInfo(asserted, retracted)
34+
end NotNullInfo
35+
36+
val noNotNulls = new NotNullInfo(Set(), Set())
1637

1738
/** A pair of not-null sets, depending on whether a condition is `true` or `false` */
18-
case class EitherExcluded(ifTrue: Excluded, ifFalse: Excluded) with
19-
def isEmpty = ifTrue.isEmpty && ifFalse.isEmpty
39+
case class NotNullConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]) with
40+
def isEmpty = this eq neitherNotNull
41+
42+
object NotNullConditional with
43+
def apply(ifTrue: Set[TermRef], ifFalse: Set[TermRef]): NotNullConditional =
44+
if ifTrue.isEmpty && ifFalse.isEmpty then neitherNotNull
45+
else new NotNullConditional(ifTrue, ifFalse)
46+
end NotNullConditional
2047

21-
val NoneExcluded = EitherExcluded(Set(), Set())
48+
val neitherNotNull = new NotNullConditional(Set(), Set())
2249

2350
/** An attachment that represents conditional flow facts established
2451
* by this tree, which represents a condition.
2552
*/
26-
private[typer] val CondExcluded = Property.StickyKey[Nullables.EitherExcluded]
53+
private[typer] val NNConditional = Property.StickyKey[NotNullConditional]
2754

2855
/** An attachment that represents unconditional flow facts established
2956
* by this tree.
3057
*/
31-
private[typer] val AlwaysExcluded = Property.StickyKey[Nullables.Excluded]
58+
private[typer] val NNInfo = Property.StickyKey[NotNullInfo]
3259

3360
/** An extractor for null comparisons */
3461
object CompareNull with
@@ -66,11 +93,11 @@ object Nullables with
6693
def isTracked(ref: TermRef)(given Context) = ref.isStable
6794

6895
def afterPatternContext(sel: Tree, pat: Tree)(given ctx: Context) = (sel, pat) match
69-
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addExcluded(Set(ref))
96+
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addNotNullRefs(Set(ref))
7097
case _ => ctx
7198

7299
def caseContext(sel: Tree, pat: Tree)(given ctx: Context): Context = sel match
73-
case TrackedRef(ref) if matchesNotNull(pat) => ctx.addExcluded(Set(ref))
100+
case TrackedRef(ref) if matchesNotNull(pat) => ctx.addNotNullRefs(Set(ref))
74101
case _ => ctx
75102

76103
private def matchesNotNull(pat: Tree)(given Context): Boolean = pat match
@@ -79,48 +106,59 @@ object Nullables with
79106
// TODO: Add constant pattern if the constant type is not nullable
80107
case _ => false
81108

82-
given (excluded: List[Excluded])
83-
def containsRef(ref: TermRef): Boolean =
84-
excluded.exists(_.contains(ref))
109+
given (infos: List[NotNullInfo])
110+
@tailRec
111+
def containsRef(ref: TermRef): Boolean = infos match
112+
case info :: infos1 =>
113+
if info.asserted.contains(ref) then true
114+
else if info.retracted.contains(ref) then false
115+
else containsRef(infos1)(ref)
116+
case _ =>
117+
false
85118

86-
def containsAll(refs: Set[TermRef]): Boolean =
87-
refs.forall(excluded.containsRef(_))
119+
def extendWith(info: NotNullInfo) =
120+
if info.asserted.forall(infos.containsRef(_))
121+
&& !info.retracted.exists(infos.containsRef(_))
122+
then infos
123+
else info :: infos
88124

89125
given (tree: Tree)
90126

91127
/* The `tree` with added attachment stating that all paths in `refs` are not-null */
92-
def withNotNullRefs(refs: Excluded): tree.type =
93-
if refs.nonEmpty then tree.putAttachment(AlwaysExcluded, refs)
128+
def withNotNullInfo(info: NotNullInfo): tree.type =
129+
if !info.isEmpty then tree.putAttachment(NNInfo, info)
94130
tree
95131

132+
def withNotNullRefs(refs: Set[TermRef]) = tree.withNotNullInfo(NotNullInfo(refs, Set()))
133+
96134
/* The paths that are known to be not null after execution of `tree` terminates normally */
97-
def notNullRefs(given Context): Excluded =
98-
stripInlined(tree).getAttachment(AlwaysExcluded) match
99-
case Some(excl) if !curCtx.erasedTypes => excl
100-
case _ => Set.empty
135+
def notNullInfo(given Context): NotNullInfo =
136+
stripInlined(tree).getAttachment(NNInfo) match
137+
case Some(info) if !curCtx.erasedTypes => info
138+
case _ => noNotNulls
101139

102140
/** The paths that are known to be not null if the condition represented
103141
* by `tree` yields `true` or `false`. Two empty sets if `tree` is not
104142
* a condition.
105143
*/
106-
def condNotNullRefs(given Context): EitherExcluded =
107-
stripBlock(tree).getAttachment(CondExcluded) match
108-
case Some(excl) if !curCtx.erasedTypes => excl
109-
case _ => NoneExcluded
144+
def notNullConditional(given Context): NotNullConditional =
145+
stripBlock(tree).getAttachment(NNConditional) match
146+
case Some(cond) if !curCtx.erasedTypes => cond
147+
case _ => neitherNotNull
110148

111149
/** The current context augmented with nullability information of `tree` */
112150
def nullableContext(given Context): Context =
113-
val excl = tree.notNullRefs
114-
if excl.isEmpty then curCtx else curCtx.addExcluded(excl)
151+
val info = tree.notNullInfo
152+
if info.isEmpty then curCtx else curCtx.addNotNullInfo(info)
115153

116154
/** The current context augmented with nullability information,
117155
* assuming the result of the condition represented by `tree` is the same as
118156
* the value of `tru`. The current context if `tree` is not a condition.
119157
*/
120158
def nullableContext(tru: Boolean)(given Context): Context =
121-
val excl = tree.condNotNullRefs
122-
if excl.isEmpty then curCtx
123-
else curCtx.addExcluded(if tru then excl.ifTrue else excl.ifFalse)
159+
val cond = tree.notNullConditional
160+
if cond.isEmpty then curCtx
161+
else curCtx.addNotNullRefs(if tru then cond.ifTrue else cond.ifFalse)
124162

125163
/** The context to use for the arguments of the function represented by `tree`.
126164
* This is the current context, augmented with nullability information
@@ -141,25 +179,26 @@ object Nullables with
141179
* 2. Boolean &&, ||, !
142180
*/
143181
def computeNullable()(given Context): tree.type =
144-
def setExcluded(ifTrue: Excluded, ifFalse: Excluded) =
145-
tree.putAttachment(CondExcluded, EitherExcluded(ifTrue, ifFalse))
146-
if !curCtx.erasedTypes then tree match
147-
case CompareNull(TrackedRef(ref), testEqual) =>
148-
if testEqual then setExcluded(Set(), Set(ref))
149-
else setExcluded(Set(ref), Set())
150-
case Apply(Select(x, _), y :: Nil) =>
151-
val xc = x.condNotNullRefs
152-
val yc = y.condNotNullRefs
153-
if !(xc.isEmpty && yc.isEmpty) then
154-
if tree.symbol == defn.Boolean_&& then
155-
setExcluded(xc.ifTrue | yc.ifTrue, xc.ifFalse & yc.ifFalse)
156-
else if tree.symbol == defn.Boolean_|| then
157-
setExcluded(xc.ifTrue & yc.ifTrue, xc.ifFalse | yc.ifFalse)
158-
case Select(x, _) if tree.symbol == defn.Boolean_! =>
159-
val xc = x.condNotNullRefs
160-
if !xc.isEmpty then
161-
setExcluded(xc.ifFalse, xc.ifTrue)
162-
case _ =>
182+
def setConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]) =
183+
tree.putAttachment(NNConditional, NotNullConditional(ifTrue, ifFalse))
184+
if !curCtx.erasedTypes then
185+
tree match
186+
case CompareNull(TrackedRef(ref), testEqual) =>
187+
if testEqual then setConditional(Set(), Set(ref))
188+
else setConditional(Set(ref), Set())
189+
case Apply(Select(x, _), y :: Nil) =>
190+
val xc = x.notNullConditional
191+
val yc = y.notNullConditional
192+
if !(xc.isEmpty && yc.isEmpty) then
193+
if tree.symbol == defn.Boolean_&& then
194+
setConditional(xc.ifTrue | yc.ifTrue, xc.ifFalse & yc.ifFalse)
195+
else if tree.symbol == defn.Boolean_|| then
196+
setConditional(xc.ifTrue & yc.ifTrue, xc.ifFalse | yc.ifFalse)
197+
case Select(x, _) if tree.symbol == defn.Boolean_! =>
198+
val xc = x.notNullConditional
199+
if !xc.isEmpty then
200+
setConditional(xc.ifFalse, xc.ifTrue)
201+
case _ =>
163202
tree
164203

165204
/** Compute nullability information for this tree and all its subtrees */

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import dotty.tools.dotc.transform.{PCPCheckAndHeal, Staging, TreeMapWithStages}
4040
import transform.SymUtils._
4141
import transform.TypeUtils._
4242
import reporting.trace
43-
import Nullables.given
43+
import Nullables.{NotNullInfo, given}
4444

4545
object Typer {
4646

@@ -634,7 +634,7 @@ class Typer extends Namer
634634
else if (isWildcard) tree.expr.withType(tpt.tpe)
635635
else typed(tree.expr, tpt.tpe.widenSkolem)
636636
assignType(cpy.Typed(tree)(expr1, tpt), underlyingTreeTpe)
637-
.withNotNullRefs(expr1.notNullRefs)
637+
.withNotNullInfo(expr1.notNullInfo)
638638
}
639639

640640
if (untpd.isWildcardStarArg(tree)) {
@@ -766,7 +766,7 @@ class Typer extends Namer
766766
ensureNoLocalRefs(
767767
cpy.Block(tree)(stats1, expr1)
768768
.withType(expr1.tpe)
769-
.withNotNullRefs(stats1.foldLeft(expr1.notNullRefs)(_ | _.notNullRefs)),
769+
.withNotNullInfo(stats1.foldRight(expr1.notNullInfo)(_.notNullInfo.seq(_))),
770770
pt, localSyms(stats1))
771771
}
772772

@@ -826,9 +826,9 @@ class Typer extends Namer
826826
assignType(cpy.If(tree)(cond1, thenp1, elsep1), thenp1, elsep1)
827827

828828
if result.thenp.tpe.isRef(defn.NothingClass) then
829-
result.withNotNullRefs(cond1.condNotNullRefs.ifFalse)
829+
result.withNotNullRefs(cond1.notNullConditional.ifFalse)
830830
else if result.elsep.tpe.isRef(defn.NothingClass) then
831-
result.withNotNullRefs(cond1.condNotNullRefs.ifTrue)
831+
result.withNotNullRefs(cond1.notNullConditional.ifTrue)
832832
else
833833
result
834834
end typedIf
@@ -1263,7 +1263,7 @@ class Typer extends Namer
12631263
else typed(tree.cond, defn.BooleanType)
12641264
val body1 = typed(tree.body, defn.UnitType)(given cond1.nullableContext(true))
12651265
assignType(cpy.WhileDo(tree)(cond1, body1))
1266-
.withNotNullRefs(cond1.condNotNullRefs.ifFalse)
1266+
.withNotNullRefs(cond1.notNullConditional.ifFalse)
12671267
}
12681268

12691269
def typedTry(tree: untpd.Try, pt: Type)(implicit ctx: Context): Try = {
@@ -1553,12 +1553,12 @@ class Typer extends Namer
15531553
def typedValDef(vdef: untpd.ValDef, sym: Symbol)(implicit ctx: Context): Tree = {
15541554
sym.infoOrCompleter match
15551555
case completer: Namer#Completer
1556-
if completer.creationContext.notNullRefs ne ctx.notNullRefs =>
1556+
if completer.creationContext.notNullInfos ne ctx.notNullInfos =>
15571557
// The RHS of a val def should know about not null facts established
15581558
// in preceding statements (unless the ValDef is completed ahead of time,
15591559
// then it is impossible).
15601560
vdef.symbol.info = Completer(completer.original)(
1561-
given completer.creationContext.withNotNullRefs(ctx.notNullRefs))
1561+
given completer.creationContext.withNotNullInfos(ctx.notNullInfos))
15621562
case _ =>
15631563

15641564
val ValDef(name, tpt, _) = vdef
@@ -2201,7 +2201,7 @@ class Typer extends Namer
22012201
def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): (List[Tree], Context) = {
22022202
val buf = new mutable.ListBuffer[Tree]
22032203
val enumContexts = new mutable.HashMap[Symbol, Context]
2204-
val initialNotNullRefs = ctx.notNullRefs
2204+
val initialNotNullInfos = ctx.notNullInfos
22052205
// A map from `enum` symbols to the contexts enclosing their definitions
22062206
@tailrec def traverse(stats: List[untpd.Tree])(implicit ctx: Context): (List[Tree], Context) = stats match {
22072207
case (imp: untpd.Import) :: rest =>
@@ -2219,7 +2219,7 @@ class Typer extends Namer
22192219
case _: ValDef if !mdef.mods.is(Lazy) && ctx.owner.isTerm =>
22202220
ctx // all preceding statements will have been executed in this case
22212221
case _ =>
2222-
ctx.withNotNullRefs(initialNotNullRefs)
2222+
ctx.withNotNullInfos(initialNotNullInfos)
22232223
typed(mdef)(given defCtx) match {
22242224
case mdef1: DefDef if !Inliner.bodyToInline(mdef1.symbol).isEmpty =>
22252225
buf += inlineExpansion(mdef1)

0 commit comments

Comments
 (0)