Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2436,8 +2436,35 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val capabilityProof = caughtExceptions.reduce(OrType(_, _, true))
untpd.Block(makeCanThrow(capabilityProof), expr)

/** Graphic explanation of NotNullInfo logic:
* Leftward exit indicates exceptional case
* Downward exit indicates normal case
*
* ┌─────┐
* │ Try ├─────┬────────┬─────┐
* └──┬──┘ ▼ ▼ │
* │ ┌───────┐┌───────┐ │
* │ │ Catch ││ Catch ├─┤
* │ └───┬───┘└───┬───┘ │
* └─┬──────┴────────┘ │
* ▼ ▼
* ┌─────────┐ ┌─────────┐
* │ Finally ├──────┐ │ Finally ├──┐
* └────┬────┘ │ └────┬────┘ │
* ▼ └─────────┴───────┴─►
* exprNNInfo = Effect of the try block if completed normally
* casesNNInfo = Effect of catch blocks completing normally
* normalAfterCasesInfo = Exceptional try followed by normal catches
* We type finalizer with normalAfterCasesInfo.retracted
*
* Overall effect of try-catch-finally =
* resNNInfo =
* (exprNNInfo OR normalAfterCasesInfo) followed by normal finally block
*
* For all nninfo, if a tree can be typed using nninfo.retractedInfo, then it can
* also be typed using nninfo.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurred to me that putting the Greek letters on the diagram is misleading. (I take the blame for originally
suggesting it.)

It is misleading because the nnInfo represents the effect of a piece of code, not the nullness information at a particular program point. The Greek letters on the diagram suggest program points.

Instead, the Greek letters should be defined below the diagram as equations. (And we could come up with suitable descriptive English names instead of Greek letters.)

\alpha = effect of try
\lambda = \alpha.retractedInfo seq ((effects of catches).reduce(alt))
\beta = \alpha alt \lambda
context for typing finally = (\alpha.retractedInfo) alt (\lambda.retractedInfo) = \beta.retractedInfo
\delta = effect of finally
\theta = (\beta seq \delta) alt (\beta.retractedInfo seq \delta seq empty.terminatedInfo)

Possible English names:
\alpha -> effectOfTry
\delta -> effectOfFinally
\theta is just the final nnInfo that we compute for the whole try-catch-finally, so maybe it doesn't need a name
\lambda and \beta I'm not sure of good names
contextForFinally would be a good name, or finallyCtx
\beta means try/catch has completed normally (possibly with an exception that was handled by the catch)
The code below uses normalNNInfo for \beta and exceptionalResolveNNInfo for finallyCtx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I did mean beta, not alpha. Another way of writing it would be:
\theta = (\alpha seq \delta) alt (\lambda seq \delta) alt (\beta.retractedInfo seq \delta seq empty.terminatedInfo)
But since \beta = \alpha alt \lambda, I think the first two cases are equivalent to \beta seq \delta.

\alpha seq \delta corresponds to the try block completing normally, then the finally block completing normally.

\lambda seq \delta corresponds to the try block throwing, the catch block catching it and completing normally, then the finally block completing normally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I realized after commenting that I mixed up some symbols and deleted my reply, sorry for the confusion.

def typedTry(tree: untpd.Try, pt: Type)(using Context): Try =
var nnInfo = NotNullInfo.empty
val expr2 :: cases2x = harmonic(harmonize, pt) {
// We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities`
// uses the types of patterns in `tree.cases` to determine the capabilities.
Expand All @@ -2450,25 +2477,23 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType)
val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto)

// Since we don't know at which point the the exception is thrown in the body,
// we have to collect any reference that is once retracted.
nnInfo = expr1.notNullInfo.retractedInfo

val casesCtx = ctx.addNotNullInfo(nnInfo)
val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo)
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx)
expr1 :: cases1
}: @unchecked
val cases2 = cases2x.asInstanceOf[List[CaseDef]]
val exprNNInfo = expr2.notNullInfo
val casesNNInfo =
cases2.map(_.notNullInfo)
.foldLeft(NotNullInfo.empty.terminatedInfo)(_.alt(_))
val normalAfterCasesInfo = exprNNInfo.retractedInfo.seq(casesNNInfo)

// It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught.
// Therefore, the code in the finalizer and after the try block can only rely on the retracted
// info from the cases' body.
if cases2.nonEmpty then
nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))

val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nnInfo))
nnInfo = nnInfo.seq(finalizer1.notNullInfo)
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nnInfo)
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(normalAfterCasesInfo.retractedInfo))
val resNNInfo = exprNNInfo.alt(normalAfterCasesInfo).seq(finalizer1.notNullInfo)
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(resNNInfo)

def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try =
val cases: List[untpd.CaseDef] = tree.handler match
Expand Down
276 changes: 275 additions & 1 deletion tests/explicit-nulls/neg/i21619.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
class SomeException extends Exception

def test1: String =
var x: String | Null = null
x = ""
Expand All @@ -13,6 +15,22 @@ def test1: String =
x.replace("", "") // error

def test2: String =
var x: String | Null = null
x = ""
var i: Int = 1
try
i match
case _ =>
x = null
throw new Exception()
x = ""
catch
case e: NoSuchMethodError =>
x = "e"
x.replace("", "") // ok

// From i24296
def test2_2: String =
var x: String | Null = null
x = ""
var i: Int = 1
Expand All @@ -25,8 +43,43 @@ def test2: String =
catch
case e: Exception =>
x = "e"
case _ =>
x.replace("", "") // error

def test2_3: String =
var x: String | Null = null
x = ""
var i: Int = 1
try
i match
case _ =>
x = null
throw new Exception()
x = ""
catch
case e: NoSuchMethodError =>
x = "e"
case e: AbstractMethodError =>
x = "e"
x.replace("", "") // ok

def test2_4: String =
var x: String | Null = null
x = ""
var i: Int = 1
try
i match
case _ =>
x = null
throw new Exception()
x = ""
catch
case e: NoSuchMethodError =>
x = "e"
case e: AbstractMethodError =>
throw new Exception()
x.replace("", "") // ok

def test3: String =
var x: String | Null = null
x = ""
Expand Down Expand Up @@ -89,4 +142,225 @@ def test6 = {
}
}
x.replace("", "") // error
}
}

// From i24296
def test7() =
var x: String | Null = null
try {
x = ""
} catch {
case e =>
throw e
}
x.trim() // ok

def test8() =
var x: String | Null = null
try {
try {
x = ""
} catch {
case e => throw e
}
} catch {
case e => throw e
}
x.trim() // ok

def test9() =
var x: String | Null = null
try {
x = ""
} catch {
case e: AssertionError =>
throw e
case _ =>
}
x.trim() // error

def test9_2() =
var x: String | Null = null
try {
x = ""
} catch {
case e: AssertionError =>
throw e
}
x.trim() // ok

def test10() =
var x: String | Null = null
try {
x = ""
} catch {
case e =>
throw e
} finally {
x = null
}
x.trim() // error

def test11() =
var x: String | Null = null
try {
x = ""
} catch {
case e =>
x = null
throw e
} finally {
x = ""
}
x.trim() // ok

def test12() =
var x: String | Null = null
try {
x = ""
} catch {
case e =>
x = null
throw e
} finally {
throw new Exception
x = ""
}
x.trim() // ok

def test12_2() =
var x: String | Null = null
try {
x = ""
} catch {
case e =>
x = null
throw e
} finally {
throw new Exception
}
x.trim() // ok

def test13() =
var x: String | Null = null
try {
x = null
throw new RuntimeException
} finally {
x.trim() // error
}
x.trim() // OK

def test14() =
var x: String | Null = ""
x = ""
try {
try {
} catch {
case e =>
x = null
throw e
}
} catch {
case e =>
x.trim() // error
}



def test15: String =
var x: String | Null = ???
var y: String | Null = ???
// ...
try
x = null
// ...
x = ""
catch
case e: SomeException =>
x = null
// situation 1: don't throw or return
// situation 2:
return ""
finally
y = x
// should always error on y.trim

y.trim // error (ideally, should error if situation 1, should not error if situation 2)

def test16: String =
var x: String | Null = ???
x = ""
try
// call some method that throws
// ...
x = ""
catch
case e: SomeException =>
x = null
// call some method that throws
// ...
x = "<error>"
finally {}

x.trim() // ok

def test17: String =
var x: String | Null = ???
x = ""
try
// call some method that throws
// ...
x = ""
catch
case e: SomeException =>
x = null
// call some method that throws
// ...
x = "<error>"
finally
println(x.trim()) // error

""

def test18: String =
var x: String | Null = ???
var y: String | Null = ???
// ...
try
x = null
y = null
// ...
x = ""
y = ""
catch
case e: SomeException =>
x = null
return ""
finally {}

x.trim + y.trim


def test19: String =
var x: String | Null = ???
try
x = null
catch
case e: SomeException =>
x = null
throw e
finally
throw new Exception()
x.trim // ok

def test20: String =
var x: String | Null = ???
x = ""
try
x = ""
catch
case e: SomeException =>
x = ""
finally {}
x.trim // ok
Loading