Skip to content

Commit f95a131

Browse files
authored
Add tracking of NotNullInfo for Match, Case, Try trees; fix #21380 (#21389)
2 parents 687deb1 + fe0bdad commit f95a131

File tree

5 files changed

+118
-9
lines changed

5 files changed

+118
-9
lines changed

Diff for: compiler/src/dotty/tools/dotc/typer/Typer.scala

+32-9
Original file line numberDiff line numberDiff line change
@@ -2135,14 +2135,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
21352135
case1
21362136
}
21372137
.asInstanceOf[List[CaseDef]]
2138-
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt)
2138+
var nni = sel.notNullInfo
2139+
if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_)))
2140+
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt).withNotNullInfo(nni)
21392141
}
21402142

21412143
// Overridden in InlineTyper for inline matches
21422144
def typedMatchFinish(tree: untpd.Match, sel: Tree, wideSelType: Type, cases: List[untpd.CaseDef], pt: Type)(using Context): Tree = {
21432145
val cases1 = harmonic(harmonize, pt)(typedCases(cases, sel, wideSelType, pt.dropIfProto))
21442146
.asInstanceOf[List[CaseDef]]
2145-
assignType(cpy.Match(tree)(sel, cases1), sel, cases1)
2147+
var nni = sel.notNullInfo
2148+
if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_)))
2149+
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).withNotNullInfo(nni)
21462150
}
21472151

21482152
def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType0: Type, pt: Type)(using Context): List[CaseDef] =
@@ -2206,17 +2210,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
22062210
}
22072211
val pat1 = indexPattern(tree).transform(pat)
22082212
val guard1 = typedExpr(tree.guard, defn.BooleanType)
2209-
var body1 = ensureNoLocalRefs(typedExpr(tree.body, pt1), pt1, ctx.scope.toList)
2213+
var body1 = ensureNoLocalRefs(
2214+
typedExpr(tree.body, pt1)(using ctx.addNotNullInfo(guard1.notNullInfoIf(true))),
2215+
pt1, ctx.scope.toList)
22102216
if ctx.gadt.isNarrowing then
22112217
// Store GADT constraint to later retrieve it (in PostTyper, for now).
22122218
// GADT constraints are necessary to correctly check bounds of type app,
22132219
// see tests/pos/i12226 and issue #12226. It might be possible that this
22142220
// will end up taking too much memory. If it does, we should just limit
22152221
// how much GADT constraints we infer - it's always sound to infer less.
22162222
pat1.putAttachment(InferredGadtConstraints, ctx.gadt)
2217-
if (pt1.isValueType) // insert a cast if body does not conform to expected type if we disregard gadt bounds
2223+
if pt1.isValueType then // insert a cast if body does not conform to expected type if we disregard gadt bounds
22182224
body1 = body1.ensureConforms(pt1)(using originalCtx)
2219-
assignType(cpy.CaseDef(tree)(pat1, guard1, body1), pat1, body1)
2225+
val nni = pat1.notNullInfo
2226+
.seq(guard1.notNullInfoIf(true))
2227+
.seq(body1.notNullInfo)
2228+
assignType(cpy.CaseDef(tree)(pat1, guard1, body1), pat1, body1).withNotNullInfo(nni)
22202229
}
22212230

22222231
val pat1 = typedPattern(tree.pat, wideSelType)(using gadtCtx)
@@ -2321,13 +2330,27 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
23212330

23222331
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
23232332
val expr2 :: cases2x = harmonic(harmonize, pt) {
2324-
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)
2325-
val expr1 = typed(addCanThrowCapabilities(tree.expr, cases1), pt.dropIfProto)
2333+
// We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities`
2334+
// uses the types of patterns in `tree.cases` to determine the capabilities.
2335+
// Hence, we create a copy of cases with empty body and type check that first, then type check
2336+
// the rest of the tree in order.
2337+
// It may seem that invalid references can be created if the type of the pattern contains
2338+
// type binds, but this is not a valid `CanThrow` capability (checked by `addCanThrowCapabilities`),
2339+
// so it is not a problem.
2340+
val casesEmptyBody1 = tree.cases.mapconserve(cpy.CaseDef(_)(body = EmptyTree))
2341+
val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType)
2342+
val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto)
2343+
val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo)
2344+
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx)
23262345
expr1 :: cases1
23272346
}: @unchecked
2328-
val finalizer1 = typed(tree.finalizer, defn.UnitType)
23292347
val cases2 = cases2x.asInstanceOf[List[CaseDef]]
2330-
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2)
2348+
2349+
var nni = expr2.notNullInfo.retractedInfo
2350+
if cases2.nonEmpty then nni = nni.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))
2351+
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nni))
2352+
nni = nni.seq(finalizer1.notNullInfo)
2353+
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nni)
23312354
}
23322355

23332356
def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try =

Diff for: compiler/test/dotc/neg-best-effort-pickling.blacklist

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ i13780-1.scala
1616
i20317a.scala
1717
i11226.scala
1818
i974.scala
19+
i13864.scala
1920

2021
# semantic db generation fails in the first compilation
2122
i1642.scala

Diff for: tests/explicit-nulls/neg/i21380.scala

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@main def test() = {
2+
var x: String | Null = null
3+
if (false) {
4+
x = ""
5+
6+
} else {
7+
x = ""
8+
}
9+
try {
10+
x = ""
11+
throw new Exception()
12+
}
13+
catch {
14+
case e: Exception => {
15+
x = null
16+
}
17+
}
18+
x.replace("", "") // error
19+
}

Diff for: tests/explicit-nulls/neg/i21380b.scala

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
def test1 =
2+
var x: String | Null = null
3+
x = ""
4+
1 match
5+
case 1 => x = null
6+
case _ => x = x.trim() // ok
7+
x.replace("", "") // error
8+
9+
def test2(i: Int) =
10+
var x: String | Null = null
11+
i match
12+
case 1 => x = "1"
13+
case _ => x = " "
14+
x.replace("", "") // ok
15+
16+
def test3(i: Int) =
17+
var x: String | Null = null
18+
i match
19+
case 1 if x != null => ()
20+
case _ => x = " "
21+
x.trim() // ok

Diff for: tests/explicit-nulls/neg/i21380c.scala

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
def test1(i: Int): Int =
2+
var x: String | Null = null
3+
if i == 0 then x = ""
4+
else x = ""
5+
try
6+
x = x.replace(" ", "") // ok
7+
throw new Exception()
8+
catch
9+
case e: Exception =>
10+
x = x.replaceAll(" ", "") // error
11+
x = null
12+
x.length // error
13+
14+
def test2: Int =
15+
var x: String | Null = null
16+
try throw new Exception()
17+
finally x = ""
18+
x.length // ok
19+
20+
def test3 =
21+
var x: String | Null = ""
22+
try throw new Exception()
23+
catch case e: Exception =>
24+
x = (??? : String | Null)
25+
finally
26+
val l = x.length // error
27+
28+
def test4: Int =
29+
var x: String | Null = null
30+
try throw new Exception()
31+
catch
32+
case npe: NullPointerException => x = ""
33+
case _ => x = ""
34+
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.
38+
39+
def test5: Int =
40+
var x: String | Null = null
41+
try
42+
x = ""
43+
throw new Exception()
44+
catch
45+
case npe: NullPointerException => val i: Int = x.length // error

0 commit comments

Comments
 (0)