Skip to content

Commit e4c225a

Browse files
authored
Merge pull request #145 from scala/backport-lts-3.3-21624
Backport "Fix scala#21619: Refactor NotNullInfo to record every reference which is retracted once." to 3.3 LTS
2 parents b5b7fa5 + 00f09ac commit e4c225a

File tree

9 files changed

+259
-80
lines changed

9 files changed

+259
-80
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -755,13 +755,13 @@ object Contexts {
755755

756756
extension (c: Context)
757757
def addNotNullInfo(info: NotNullInfo) =
758-
c.withNotNullInfos(c.notNullInfos.extendWith(info))
758+
if c.explicitNulls then c.withNotNullInfos(c.notNullInfos.extendWith(info)) else c
759759

760760
def addNotNullRefs(refs: Set[TermRef]) =
761-
c.addNotNullInfo(NotNullInfo(refs, Set()))
761+
if c.explicitNulls then c.addNotNullInfo(NotNullInfo(refs, Set())) else c
762762

763763
def withNotNullInfos(infos: List[NotNullInfo]): Context =
764-
if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos)
764+
if !c.explicitNulls || (c.notNullInfos eq infos) then c else c.fresh.setNotNullInfos(infos)
765765

766766
def relaxedOverrideContext: Context =
767767
c.withModeBits(c.mode &~ Mode.SafeNulls | Mode.RelaxedOverriding)

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,7 @@ trait Applications extends Compatibility {
10581058
case _ => ()
10591059
else ()
10601060

1061-
fun1.tpe match {
1061+
val result = fun1.tpe match {
10621062
case err: ErrorType => cpy.Apply(tree)(fun1, proto.typedArgs()).withType(err)
10631063
case TryDynamicCallType =>
10641064
val isInsertedApply = fun1 match {
@@ -1132,6 +1132,11 @@ trait Applications extends Compatibility {
11321132
else tryWithImplicitOnQualifier(fun1, proto).getOrElse(fail))
11331133
}
11341134
}
1135+
1136+
if result.tpe.isNothingType then
1137+
val nnInfo = result.notNullInfo
1138+
result.withNotNullInfo(nnInfo.terminatedInfo)
1139+
else result
11351140
}
11361141

11371142
/** Convert expression like

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

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,46 @@ object Nullables:
4848
val newHi = if needNullifyHi(lo.typeOpt, hiTpe) then TypeTree(OrType(hiTpe, defn.NullType, soft = false)) else hi
4949
TypeBoundsTree(lo, newHi, alias)
5050

51-
/** A set of val or var references that are known to be not null, plus a set of
52-
* variable references that are not known (anymore) to be not null
51+
/** A set of val or var references that are known to be not null
52+
* after the tree finishes executing normally (non-exceptionally),
53+
* plus a set of variable references that are ever assigned to null,
54+
* and may therefore be null if execution of the tree is interrupted
55+
* by an exception.
5356
*/
54-
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]):
55-
assert((asserted & retracted).isEmpty)
56-
57+
case class NotNullInfo(asserted: Set[TermRef] | Null, retracted: Set[TermRef]):
5758
def isEmpty = this eq NotNullInfo.empty
5859

5960
def retractedInfo = NotNullInfo(Set(), retracted)
6061

62+
def terminatedInfo = NotNullInfo(null, retracted)
63+
6164
/** The sequential combination with another not-null info */
6265
def seq(that: NotNullInfo): NotNullInfo =
6366
if this.isEmpty then that
6467
else if that.isEmpty then this
65-
else NotNullInfo(
66-
this.asserted.union(that.asserted).diff(that.retracted),
67-
this.retracted.union(that.retracted).diff(that.asserted))
68+
else
69+
val newAsserted =
70+
if this.asserted == null || that.asserted == null then null
71+
else this.asserted.diff(that.retracted).union(that.asserted)
72+
val newRetracted = this.retracted.union(that.retracted)
73+
NotNullInfo(newAsserted, newRetracted)
6874

6975
/** The alternative path combination with another not-null info. Used to merge
70-
* the nullability info of the two branches of an if.
76+
* the nullability info of the branches of an if or match.
7177
*/
7278
def alt(that: NotNullInfo): NotNullInfo =
73-
NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted))
79+
val newAsserted =
80+
if this.asserted == null then that.asserted
81+
else if that.asserted == null then this.asserted
82+
else this.asserted.intersect(that.asserted)
83+
val newRetracted = this.retracted.union(that.retracted)
84+
NotNullInfo(newAsserted, newRetracted)
85+
end NotNullInfo
7486

7587
object NotNullInfo:
7688
val empty = new NotNullInfo(Set(), Set())
77-
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
78-
if asserted.isEmpty && retracted.isEmpty then empty
89+
def apply(asserted: Set[TermRef] | Null, retracted: Set[TermRef]): NotNullInfo =
90+
if asserted != null && asserted.isEmpty && retracted.isEmpty then empty
7991
else new NotNullInfo(asserted, retracted)
8092
end NotNullInfo
8193

@@ -198,7 +210,7 @@ object Nullables:
198210
*/
199211
@tailrec def impliesNotNull(ref: TermRef): Boolean = infos match
200212
case info :: infos1 =>
201-
if info.asserted.contains(ref) then true
213+
if info.asserted == null || info.asserted.contains(ref) then true
202214
else if info.retracted.contains(ref) then false
203215
else infos1.impliesNotNull(ref)
204216
case _ =>
@@ -208,16 +220,15 @@ object Nullables:
208220
* or retractions in `info` supersede infos in existing entries of `infos`.
209221
*/
210222
def extendWith(info: NotNullInfo) =
211-
if info.isEmpty
212-
|| info.asserted.forall(infos.impliesNotNull(_))
213-
&& !info.retracted.exists(infos.impliesNotNull(_))
214-
then infos
223+
if info.isEmpty then infos
215224
else info :: infos
216225

217226
/** Retract all references to mutable variables */
218227
def retractMutables(using Context) =
219-
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
220-
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
228+
val mutables = infos.foldLeft(Set[TermRef]()):
229+
(ms, info) => ms.union(
230+
if info.asserted == null then Set.empty
231+
else info.asserted.filter(_.symbol.is(Mutable)))
221232
infos.extendWith(NotNullInfo(Set(), mutables))
222233

223234
end extension
@@ -279,15 +290,35 @@ object Nullables:
279290
extension (tree: Tree)
280291

281292
/* The `tree` with added nullability attachment */
282-
def withNotNullInfo(info: NotNullInfo): tree.type =
283-
if !info.isEmpty then tree.putAttachment(NNInfo, info)
293+
def withNotNullInfo(info: NotNullInfo)(using Context): tree.type =
294+
if ctx.explicitNulls && !info.isEmpty then tree.putAttachment(NNInfo, info)
284295
tree
285296

297+
/* Collect the nullability info from parts of `tree` */
298+
def collectNotNullInfo(using Context): NotNullInfo = tree match
299+
case Typed(expr, _) =>
300+
expr.notNullInfo
301+
case Apply(fn, args) =>
302+
val argsInfo = args.map(_.notNullInfo)
303+
val fnInfo = fn.notNullInfo
304+
argsInfo.foldLeft(fnInfo)(_ seq _)
305+
case TypeApply(fn, _) =>
306+
fn.notNullInfo
307+
case _ =>
308+
// Other cases are handled specially in typer.
309+
NotNullInfo.empty
310+
286311
/* The nullability info of `tree` */
287312
def notNullInfo(using Context): NotNullInfo =
288-
stripInlined(tree).getAttachment(NNInfo) match
289-
case Some(info) if !ctx.erasedTypes => info
290-
case _ => NotNullInfo.empty
313+
if !ctx.explicitNulls then NotNullInfo.empty
314+
else
315+
val tree1 = stripInlined(tree)
316+
tree1.getAttachment(NNInfo) match
317+
case Some(info) if !ctx.erasedTypes => info
318+
case _ =>
319+
val nnInfo = tree1.collectNotNullInfo
320+
tree1.withNotNullInfo(nnInfo)
321+
nnInfo
291322

292323
/* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */
293324
def notNullInfoIf(c: Boolean)(using Context): NotNullInfo =
@@ -368,21 +399,23 @@ object Nullables:
368399
end extension
369400

370401
extension (tree: Assign)
371-
def computeAssignNullable()(using Context): tree.type = tree.lhs match
372-
case TrackedRef(ref) =>
373-
val rhstp = tree.rhs.typeOpt
374-
if ctx.explicitNulls && ref.isNullableUnion then
375-
if rhstp.isNullType || rhstp.isNullableUnion then
376-
// If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the
377-
// lhs variable is no longer trackable. We don't need to check whether the type `T`
378-
// is correct here, as typer will check it.
379-
tree.withNotNullInfo(NotNullInfo(Set(), Set(ref)))
380-
else
381-
// If the initial type is nullable and the assigned value is non-null,
382-
// we add it to the NotNull.
383-
tree.withNotNullInfo(NotNullInfo(Set(ref), Set()))
384-
else tree
385-
case _ => tree
402+
def computeAssignNullable()(using Context): tree.type =
403+
var nnInfo = tree.rhs.notNullInfo
404+
tree.lhs match
405+
case TrackedRef(ref) if ctx.explicitNulls && ref.isNullableUnion =>
406+
nnInfo = nnInfo.seq:
407+
val rhstp = tree.rhs.typeOpt
408+
if rhstp.isNullType || rhstp.isNullableUnion then
409+
// If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the
410+
// lhs variable is no longer trackable. We don't need to check whether the type `T`
411+
// is correct here, as typer will check it.
412+
NotNullInfo(Set(), Set(ref))
413+
else
414+
// If the initial type is nullable and the assigned value is non-null,
415+
// we add it to the NotNull.
416+
NotNullInfo(Set(ref), Set())
417+
case _ =>
418+
tree.withNotNullInfo(nnInfo)
386419
end extension
387420

388421
private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!)
@@ -490,7 +523,10 @@ object Nullables:
490523
&& assignmentSpans.getOrElse(sym.span.start, Nil).exists(whileSpan.contains(_))
491524
&& ctx.notNullInfos.impliesNotNull(ref)
492525

493-
val retractedVars = ctx.notNullInfos.flatMap(_.asserted.filter(isRetracted)).toSet
526+
val retractedVars = ctx.notNullInfos.flatMap(info =>
527+
if info.asserted == null then Set.empty
528+
else info.asserted.filter(isRetracted)
529+
).toSet
494530
ctx.addNotNullInfo(NotNullInfo(Set(), retractedVars))
495531
end whileContext
496532

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

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
10001000
untpd.unsplice(tree.expr).putAttachment(AscribedToUnit, ())
10011001
typed(tree.expr, underlyingTreeTpe.tpe.widenSkolem)
10021002
assignType(cpy.Typed(tree)(expr1, tpt), underlyingTreeTpe)
1003-
.withNotNullInfo(expr1.notNullInfo)
10041003
}
10051004

10061005
if (untpd.isWildcardStarArg(tree)) {
@@ -1333,11 +1332,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
13331332

13341333
def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo)
13351334
def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo)
1336-
result.withNotNullInfo(
1337-
if result.thenp.tpe.isRef(defn.NothingClass) then elsePathInfo
1338-
else if result.elsep.tpe.isRef(defn.NothingClass) then thenPathInfo
1339-
else thenPathInfo.alt(elsePathInfo)
1340-
)
1335+
result.withNotNullInfo(thenPathInfo.alt(elsePathInfo))
13411336
end typedIf
13421337

13431338
/** Decompose function prototype into a list of parameter prototypes and a result
@@ -1860,20 +1855,25 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
18601855
case1
18611856
}
18621857
.asInstanceOf[List[CaseDef]]
1863-
var nni = sel.notNullInfo
1864-
if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_)))
1865-
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt).withNotNullInfo(nni)
1858+
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt)
1859+
.withNotNullInfo(notNullInfoFromCases(sel.notNullInfo, cases1))
18661860
}
18671861

18681862
// Overridden in InlineTyper for inline matches
18691863
def typedMatchFinish(tree: untpd.Match, sel: Tree, wideSelType: Type, cases: List[untpd.CaseDef], pt: Type)(using Context): Tree = {
18701864
val cases1 = harmonic(harmonize, pt)(typedCases(cases, sel, wideSelType, pt.dropIfProto))
18711865
.asInstanceOf[List[CaseDef]]
1872-
var nni = sel.notNullInfo
1873-
if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_)))
1874-
assignType(cpy.Match(tree)(sel, cases1), sel, cases1).withNotNullInfo(nni)
1866+
assignType(cpy.Match(tree)(sel, cases1), sel, cases1)
1867+
.withNotNullInfo(notNullInfoFromCases(sel.notNullInfo, cases1))
18751868
}
18761869

1870+
private def notNullInfoFromCases(initInfo: NotNullInfo, cases: List[CaseDef])(using Context): NotNullInfo =
1871+
if cases.isEmpty then
1872+
// Empty cases is not allowed for match tree in the source code,
1873+
// but it can be generated by inlining: `tests/pos/i19198.scala`.
1874+
initInfo
1875+
else cases.map(_.notNullInfo).reduce(_.alt(_))
1876+
18771877
def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType: Type, pt: Type)(using Context): List[CaseDef] =
18781878
var caseCtx = ctx
18791879
cases.mapconserve { cas =>
@@ -1960,7 +1960,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
19601960
def typedLabeled(tree: untpd.Labeled)(using Context): Labeled = {
19611961
val bind1 = typedBind(tree.bind, WildcardType).asInstanceOf[Bind]
19621962
val expr1 = typed(tree.expr, bind1.symbol.info)
1963-
assignType(cpy.Labeled(tree)(bind1, expr1))
1963+
assignType(cpy.Labeled(tree)(bind1, expr1)).withNotNullInfo(expr1.notNullInfo.retractedInfo)
19641964
}
19651965

19661966
/** Type a case of a type match */
@@ -2010,7 +2010,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20102010
// Hence no adaptation is possible, and we assume WildcardType as prototype.
20112011
(from, proto)
20122012
val expr1 = typedExpr(tree.expr orElse untpd.syntheticUnitLiteral.withSpan(tree.span), proto)
2013-
assignType(cpy.Return(tree)(expr1, from))
2013+
assignType(cpy.Return(tree)(expr1, from)).withNotNullInfo(expr1.notNullInfo.terminatedInfo)
20142014
end typedReturn
20152015

20162016
def typedWhileDo(tree: untpd.WhileDo)(using Context): Tree =
@@ -2051,7 +2051,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20512051
val capabilityProof = caughtExceptions.reduce(OrType(_, _, true))
20522052
untpd.Block(makeCanThrow(capabilityProof), expr)
20532053

2054-
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
2054+
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try =
2055+
var nnInfo = NotNullInfo.empty
20552056
val expr2 :: cases2x = harmonic(harmonize, pt) {
20562057
// We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities`
20572058
// uses the types of patterns in `tree.cases` to determine the capabilities.
@@ -2063,18 +2064,26 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20632064
val casesEmptyBody1 = tree.cases.mapconserve(cpy.CaseDef(_)(body = EmptyTree))
20642065
val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType)
20652066
val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto)
2066-
val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo)
2067+
2068+
// Since we don't know at which point the the exception is thrown in the body,
2069+
// we have to collect any reference that is once retracted.
2070+
nnInfo = expr1.notNullInfo.retractedInfo
2071+
2072+
val casesCtx = ctx.addNotNullInfo(nnInfo)
20672073
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx)
20682074
expr1 :: cases1
20692075
}: @unchecked
20702076
val cases2 = cases2x.asInstanceOf[List[CaseDef]]
20712077

2072-
var nni = expr2.notNullInfo.retractedInfo
2073-
if cases2.nonEmpty then nni = nni.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))
2074-
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nni))
2075-
nni = nni.seq(finalizer1.notNullInfo)
2076-
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nni)
2077-
}
2078+
// It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught.
2079+
// Therefore, the code in the finalizer and after the try block can only rely on the retracted
2080+
// info from the cases' body.
2081+
if cases2.nonEmpty then
2082+
nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_)))
2083+
2084+
val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nnInfo))
2085+
nnInfo = nnInfo.seq(finalizer1.notNullInfo)
2086+
assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nnInfo)
20782087

20792088
def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try =
20802089
val cases: List[untpd.CaseDef] = tree.handler match
@@ -2088,15 +2097,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20882097
def typedThrow(tree: untpd.Throw)(using Context): Tree =
20892098
val expr1 = typed(tree.expr, defn.ThrowableType)
20902099
val cap = checkCanThrow(expr1.tpe.widen, tree.span)
2091-
val res = Throw(expr1).withSpan(tree.span)
2100+
var res = Throw(expr1).withSpan(tree.span)
20922101
if Feature.ccEnabled && !cap.isEmpty && !ctx.isAfterTyper then
20932102
// Record access to the CanThrow capabulity recovered in `cap` by wrapping
2094-
// the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotatoon.
2095-
Typed(res,
2103+
// the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotation.
2104+
res = Typed(res,
20962105
TypeTree(
20972106
AnnotatedType(res.tpe,
20982107
Annotation(defn.RequiresCapabilityAnnot, cap, tree.span))))
2099-
else res
2108+
res.withNotNullInfo(expr1.notNullInfo.terminatedInfo)
21002109

21012110
def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = {
21022111
val elemProto = pt.stripNull.elemType match {
@@ -2507,6 +2516,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25072516
val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym)
25082517
postProcessInfo(vdef1, sym)
25092518
vdef1.setDefTree
2519+
val nnInfo = rhs1.notNullInfo
2520+
vdef1.withNotNullInfo(if sym.is(Lazy) then nnInfo.retractedInfo else nnInfo)
25102521
}
25112522

25122523
private def retractDefDef(sym: Symbol)(using Context): Tree =

tests/explicit-nulls/neg/i21380b.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,22 @@ def test3(i: Int) =
1818
i match
1919
case 1 if x != null => ()
2020
case _ => x = " "
21-
x.trim() // error // LTS specific
21+
x.trim() // error // LTS specific
22+
23+
def test4(i: Int) =
24+
var x: String | Null = null
25+
var y: String | Null = null
26+
i match
27+
case 1 => x = "1"
28+
case _ => y = " "
29+
x.trim() // error
30+
31+
def test5(i: Int): String =
32+
var x: String | Null = null
33+
var y: String | Null = null
34+
i match
35+
case 1 => x = "1"
36+
case _ =>
37+
y = " "
38+
return y // error // LTS specific
39+
x.trim() // error // LTS specific

0 commit comments

Comments
 (0)