Skip to content

Commit cdbbe5a

Browse files
committed
Refactor NotNullInfo to record every reference which is retracted once.
1 parent 4293a45 commit cdbbe5a

File tree

4 files changed

+100
-15
lines changed

4 files changed

+100
-15
lines changed

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

+23-9
Original file line numberDiff line numberDiff line change
@@ -52,35 +52,49 @@ object Nullables:
5252
val hiTree = if(hiTpe eq hi.typeOpt) hi else TypeTree(hiTpe)
5353
TypeBoundsTree(lo, hiTree, alias)
5454

55-
/** A set of val or var references that are known to be not null, plus a set of
56-
* variable references that are not known (anymore) to be not null
55+
/** A set of val or var references that are known to be not null,
56+
* a set of variable references that are not known (anymore) to be not null,
57+
* plus a set of variables that are known to be not null at any point.
5758
*/
58-
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]):
59+
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]):
5960
assert((asserted & retracted).isEmpty)
61+
assert(retracted.subsetOf(onceRetracted))
6062

6163
def isEmpty = this eq NotNullInfo.empty
6264

63-
def retractedInfo = NotNullInfo(Set(), retracted)
65+
def retractedInfo = NotNullInfo(Set(), retracted, onceRetracted)
66+
67+
def onceRetractedInfo = NotNullInfo(Set(), onceRetracted, onceRetracted)
6468

6569
/** The sequential combination with another not-null info */
6670
def seq(that: NotNullInfo): NotNullInfo =
6771
if this.isEmpty then that
6872
else if that.isEmpty then this
6973
else NotNullInfo(
7074
this.asserted.union(that.asserted).diff(that.retracted),
71-
this.retracted.union(that.retracted).diff(that.asserted))
75+
this.retracted.union(that.retracted).diff(that.asserted),
76+
this.onceRetracted.union(that.onceRetracted))
7277

7378
/** The alternative path combination with another not-null info. Used to merge
7479
* the nullability info of the two branches of an if.
7580
*/
7681
def alt(that: NotNullInfo): NotNullInfo =
77-
NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted))
82+
NotNullInfo(
83+
this.asserted.intersect(that.asserted),
84+
this.retracted.union(that.retracted),
85+
this.onceRetracted.union(that.onceRetracted))
86+
87+
def withOnceRetracted(that: NotNullInfo): NotNullInfo =
88+
if that.isEmpty then this
89+
else NotNullInfo(this.asserted, this.retracted, this.onceRetracted.union(that.onceRetracted))
7890

7991
object NotNullInfo:
80-
val empty = new NotNullInfo(Set(), Set())
92+
val empty = new NotNullInfo(Set(), Set(), Set())
8193
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
82-
if asserted.isEmpty && retracted.isEmpty then empty
83-
else new NotNullInfo(asserted, retracted)
94+
apply(asserted, retracted, retracted)
95+
def apply(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]): NotNullInfo =
96+
if asserted.isEmpty && onceRetracted.isEmpty then empty
97+
else new NotNullInfo(asserted, retracted, onceRetracted)
8498
end NotNullInfo
8599

86100
/** A pair of not-null sets, depending on whether a condition is `true` or `false` */

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -1543,8 +1543,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
15431543
def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo)
15441544
def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo)
15451545
result.withNotNullInfo(
1546-
if result.thenp.tpe.isRef(defn.NothingClass) then elsePathInfo
1547-
else if result.elsep.tpe.isRef(defn.NothingClass) then thenPathInfo
1546+
if result.thenp.tpe.isRef(defn.NothingClass) then
1547+
elsePathInfo.withOnceRetracted(thenPathInfo)
1548+
else if result.elsep.tpe.isRef(defn.NothingClass) then
1549+
thenPathInfo.withOnceRetracted(elsePathInfo)
15481550
else thenPathInfo.alt(elsePathInfo)
15491551
)
15501552
end typedIf
@@ -2353,10 +2355,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
23532355
}: @unchecked
23542356
val cases2 = cases2x.asInstanceOf[List[CaseDef]]
23552357

2356-
var nni = expr2.notNullInfo.retractedInfo
2358+
// Since we don't know at which point the the exception is thrown in the body,
2359+
// we have to collect any reference that is once retracted.
2360+
var nni = expr2.notNullInfo.onceRetractedInfo
2361+
// It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught.
2362+
// Therefore, the code in the finallizer and after the try block can only rely on the retracted
2363+
// info from the cases' body.
23572364
if cases2.nonEmpty then nni = nni.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))
2365+
23582366
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nni))
23592367
nni = nni.seq(finalizer1.notNullInfo)
2368+
23602369
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nni)
23612370
}
23622371

tests/explicit-nulls/neg/i21380c.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ def test4: Int =
3232
case npe: NullPointerException => x = ""
3333
case _ => x = ""
3434
x.length // error
35-
// Although the catch block here is exhaustive,
36-
// it is possible that the exception is thrown and not caught.
37-
// Therefore, the code after the try block can only rely on the retracted info.
35+
// Although the catch block here is exhaustive, it is possible to have non-exhaustive cases,
36+
// and some exceptions are thrown and not caught. Therefore, the code in the finallizer and
37+
// after the try block can only rely on the retracted info from the cases' body.
3838

3939
def test5: Int =
4040
var x: String | Null = null

tests/explicit-nulls/neg/i21619.scala

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
def test1: String =
2+
var x: String | Null = null
3+
x = ""
4+
var i: Int = 1
5+
try
6+
i match
7+
case _ =>
8+
x = null
9+
throw new Exception()
10+
x = ""
11+
catch
12+
case e: Exception =>
13+
x.replace("", "") // error
14+
15+
def test2: String =
16+
var x: String | Null = null
17+
x = ""
18+
var i: Int = 1
19+
try
20+
i match
21+
case _ =>
22+
x = null
23+
throw new Exception()
24+
x = ""
25+
catch
26+
case e: Exception =>
27+
x = "e"
28+
x.replace("", "") // error
29+
30+
def test3: String =
31+
var x: String | Null = null
32+
x = ""
33+
var i: Int = 1
34+
try
35+
i match
36+
case _ =>
37+
x = null
38+
throw new Exception()
39+
x = ""
40+
catch
41+
case e: Exception =>
42+
finally
43+
x = "f"
44+
x.replace("", "") // ok
45+
46+
def test4: String =
47+
var x: String | Null = null
48+
x = ""
49+
var i: Int = 1
50+
try
51+
try
52+
if i == 1 then
53+
x = null
54+
throw new Exception()
55+
else
56+
x = ""
57+
catch
58+
case _ =>
59+
x = ""
60+
catch
61+
case _ =>
62+
x.replace("", "") // error

0 commit comments

Comments
 (0)