Skip to content

Commit 42bd780

Browse files
authored
Record NotNullInfo for exceptional try-catch (#24320)
Fixes #24296 This PR modifies the logic for recording notNullInfo in try-catch-finally blocks, motivated by repeated pattern from community build sconfig ([source](https://github.com/dotty-staging/sconfig/blob/7b66d352e0b75cd46198432037366c120ad19476/sconfig/shared/src/main/scala/org/ekrich/config/impl/ConfigImpl.scala#L86)): ```scala def computeCachedConfig( loader: ClassLoader, key: String, updater: Callable[Config] ): Config = { var cache: LoaderCache = null try { cache = LoaderCacheHolder.cache } catch { case e: ExceptionInInitializerError => throw ConfigImplUtil.extractInitializerError(e) } cache.getOrElseUpdate(loader, key, updater) } ``` Here, the cache will never be null. Because all cases of the catch block resolve exceptionally, no matter where in the try block an exception occurs, once an exception is thrown, the entire context will resolve exceptionally. Hence, we can use the notNullInfo of the try block to type what comes after the catch.
2 parents 9ba8854 + 0dc91e2 commit 42bd780

File tree

2 files changed

+312
-13
lines changed

2 files changed

+312
-13
lines changed

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2472,8 +2472,35 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
24722472
val capabilityProof = caughtExceptions.reduce(OrType(_, _, true))
24732473
untpd.Block(makeCanThrow(capabilityProof), expr)
24742474

2475+
/** Graphic explanation of NotNullInfo logic:
2476+
* Leftward exit indicates exceptional case
2477+
* Downward exit indicates normal case
2478+
*
2479+
* ┌─────┐
2480+
* │ Try ├─────┬────────┬─────┐
2481+
* └──┬──┘ ▼ ▼ │
2482+
* │ ┌───────┐┌───────┐ │
2483+
* │ │ Catch ││ Catch ├─┤
2484+
* │ └───┬───┘└───┬───┘ │
2485+
* └─┬──────┴────────┘ │
2486+
* ▼ ▼
2487+
* ┌─────────┐ ┌─────────┐
2488+
* │ Finally ├──────┐ │ Finally ├──┐
2489+
* └────┬────┘ │ └────┬────┘ │
2490+
* ▼ └─────────┴───────┴─►
2491+
* exprNNInfo = Effect of the try block if completed normally
2492+
* casesNNInfo = Effect of catch blocks completing normally
2493+
* normalAfterCasesInfo = Exceptional try followed by normal catches
2494+
* We type finalizer with normalAfterCasesInfo.retracted
2495+
*
2496+
* Overall effect of try-catch-finally =
2497+
* resNNInfo =
2498+
* (exprNNInfo OR normalAfterCasesInfo) followed by normal finally block
2499+
*
2500+
* For all nninfo, if a tree can be typed using nninfo.retractedInfo, then it can
2501+
* also be typed using nninfo.
2502+
*/
24752503
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try =
2476-
var nnInfo = NotNullInfo.empty
24772504
val expr2 :: cases2x = harmonic(harmonize, pt) {
24782505
// We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities`
24792506
// uses the types of patterns in `tree.cases` to determine the capabilities.
@@ -2486,25 +2513,23 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
24862513
val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType)
24872514
val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto)
24882515

2489-
// Since we don't know at which point the the exception is thrown in the body,
2490-
// we have to collect any reference that is once retracted.
2491-
nnInfo = expr1.notNullInfo.retractedInfo
2492-
2493-
val casesCtx = ctx.addNotNullInfo(nnInfo)
2516+
val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo)
24942517
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx)
24952518
expr1 :: cases1
24962519
}: @unchecked
24972520
val cases2 = cases2x.asInstanceOf[List[CaseDef]]
2521+
val exprNNInfo = expr2.notNullInfo
2522+
val casesNNInfo =
2523+
cases2.map(_.notNullInfo)
2524+
.foldLeft(NotNullInfo.empty.terminatedInfo)(_.alt(_))
2525+
val normalAfterCasesInfo = exprNNInfo.retractedInfo.seq(casesNNInfo)
24982526

24992527
// It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught.
25002528
// Therefore, the code in the finalizer and after the try block can only rely on the retracted
25012529
// info from the cases' body.
2502-
if cases2.nonEmpty then
2503-
nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))
2504-
2505-
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nnInfo))
2506-
nnInfo = nnInfo.seq(finalizer1.notNullInfo)
2507-
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nnInfo)
2530+
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(normalAfterCasesInfo.retractedInfo))
2531+
val resNNInfo = exprNNInfo.alt(normalAfterCasesInfo).seq(finalizer1.notNullInfo)
2532+
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(resNNInfo)
25082533

25092534
def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try =
25102535
val cases: List[untpd.CaseDef] = tree.handler match

tests/explicit-nulls/neg/i21619.scala

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
class SomeException extends Exception
2+
13
def test1: String =
24
var x: String | Null = null
35
x = ""
@@ -13,6 +15,22 @@ def test1: String =
1315
x.replace("", "") // error
1416

1517
def test2: String =
18+
var x: String | Null = null
19+
x = ""
20+
var i: Int = 1
21+
try
22+
i match
23+
case _ =>
24+
x = null
25+
throw new Exception()
26+
x = ""
27+
catch
28+
case e: NoSuchMethodError =>
29+
x = "e"
30+
x.replace("", "") // ok
31+
32+
// From i24296
33+
def test2_2: String =
1634
var x: String | Null = null
1735
x = ""
1836
var i: Int = 1
@@ -25,8 +43,43 @@ def test2: String =
2543
catch
2644
case e: Exception =>
2745
x = "e"
46+
case _ =>
2847
x.replace("", "") // error
2948

49+
def test2_3: String =
50+
var x: String | Null = null
51+
x = ""
52+
var i: Int = 1
53+
try
54+
i match
55+
case _ =>
56+
x = null
57+
throw new Exception()
58+
x = ""
59+
catch
60+
case e: NoSuchMethodError =>
61+
x = "e"
62+
case e: AbstractMethodError =>
63+
x = "e"
64+
x.replace("", "") // ok
65+
66+
def test2_4: String =
67+
var x: String | Null = null
68+
x = ""
69+
var i: Int = 1
70+
try
71+
i match
72+
case _ =>
73+
x = null
74+
throw new Exception()
75+
x = ""
76+
catch
77+
case e: NoSuchMethodError =>
78+
x = "e"
79+
case e: AbstractMethodError =>
80+
throw new Exception()
81+
x.replace("", "") // ok
82+
3083
def test3: String =
3184
var x: String | Null = null
3285
x = ""
@@ -89,4 +142,225 @@ def test6 = {
89142
}
90143
}
91144
x.replace("", "") // error
92-
}
145+
}
146+
147+
// From i24296
148+
def test7() =
149+
var x: String | Null = null
150+
try {
151+
x = ""
152+
} catch {
153+
case e =>
154+
throw e
155+
}
156+
x.trim() // ok
157+
158+
def test8() =
159+
var x: String | Null = null
160+
try {
161+
try {
162+
x = ""
163+
} catch {
164+
case e => throw e
165+
}
166+
} catch {
167+
case e => throw e
168+
}
169+
x.trim() // ok
170+
171+
def test9() =
172+
var x: String | Null = null
173+
try {
174+
x = ""
175+
} catch {
176+
case e: AssertionError =>
177+
throw e
178+
case _ =>
179+
}
180+
x.trim() // error
181+
182+
def test9_2() =
183+
var x: String | Null = null
184+
try {
185+
x = ""
186+
} catch {
187+
case e: AssertionError =>
188+
throw e
189+
}
190+
x.trim() // ok
191+
192+
def test10() =
193+
var x: String | Null = null
194+
try {
195+
x = ""
196+
} catch {
197+
case e =>
198+
throw e
199+
} finally {
200+
x = null
201+
}
202+
x.trim() // error
203+
204+
def test11() =
205+
var x: String | Null = null
206+
try {
207+
x = ""
208+
} catch {
209+
case e =>
210+
x = null
211+
throw e
212+
} finally {
213+
x = ""
214+
}
215+
x.trim() // ok
216+
217+
def test12() =
218+
var x: String | Null = null
219+
try {
220+
x = ""
221+
} catch {
222+
case e =>
223+
x = null
224+
throw e
225+
} finally {
226+
throw new Exception
227+
x = ""
228+
}
229+
x.trim() // ok
230+
231+
def test12_2() =
232+
var x: String | Null = null
233+
try {
234+
x = ""
235+
} catch {
236+
case e =>
237+
x = null
238+
throw e
239+
} finally {
240+
throw new Exception
241+
}
242+
x.trim() // ok
243+
244+
def test13() =
245+
var x: String | Null = null
246+
try {
247+
x = null
248+
throw new RuntimeException
249+
} finally {
250+
x.trim() // error
251+
}
252+
x.trim() // OK
253+
254+
def test14() =
255+
var x: String | Null = ""
256+
x = ""
257+
try {
258+
try {
259+
} catch {
260+
case e =>
261+
x = null
262+
throw e
263+
}
264+
} catch {
265+
case e =>
266+
x.trim() // error
267+
}
268+
269+
270+
271+
def test15: String =
272+
var x: String | Null = ???
273+
var y: String | Null = ???
274+
// ...
275+
try
276+
x = null
277+
// ...
278+
x = ""
279+
catch
280+
case e: SomeException =>
281+
x = null
282+
// situation 1: don't throw or return
283+
// situation 2:
284+
return ""
285+
finally
286+
y = x
287+
// should always error on y.trim
288+
289+
y.trim // error (ideally, should error if situation 1, should not error if situation 2)
290+
291+
def test16: String =
292+
var x: String | Null = ???
293+
x = ""
294+
try
295+
// call some method that throws
296+
// ...
297+
x = ""
298+
catch
299+
case e: SomeException =>
300+
x = null
301+
// call some method that throws
302+
// ...
303+
x = "<error>"
304+
finally {}
305+
306+
x.trim() // ok
307+
308+
def test17: String =
309+
var x: String | Null = ???
310+
x = ""
311+
try
312+
// call some method that throws
313+
// ...
314+
x = ""
315+
catch
316+
case e: SomeException =>
317+
x = null
318+
// call some method that throws
319+
// ...
320+
x = "<error>"
321+
finally
322+
println(x.trim()) // error
323+
324+
""
325+
326+
def test18: String =
327+
var x: String | Null = ???
328+
var y: String | Null = ???
329+
// ...
330+
try
331+
x = null
332+
y = null
333+
// ...
334+
x = ""
335+
y = ""
336+
catch
337+
case e: SomeException =>
338+
x = null
339+
return ""
340+
finally {}
341+
342+
x.trim + y.trim
343+
344+
345+
def test19: String =
346+
var x: String | Null = ???
347+
try
348+
x = null
349+
catch
350+
case e: SomeException =>
351+
x = null
352+
throw e
353+
finally
354+
throw new Exception()
355+
x.trim // ok
356+
357+
def test20: String =
358+
var x: String | Null = ???
359+
x = ""
360+
try
361+
x = ""
362+
catch
363+
case e: SomeException =>
364+
x = ""
365+
finally {}
366+
x.trim // ok

0 commit comments

Comments
 (0)