Skip to content

Commit 46fc5cd

Browse files
authored
Merge pull request #47 from noti0na1/dotty-explicit-nulls-notNull
Implement flow typing using nullability check; Update documents
2 parents a423df5 + 61ca5e6 commit 46fc5cd

35 files changed

+1519
-202
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ class Definitions {
269269
@tu lazy val Any_asInstanceOf: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOf_, _.paramRefs(0), Final)
270270
@tu lazy val Any_typeTest: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.isInstanceOfPM, _ => BooleanType, Final | Synthetic | Artifact)
271271
@tu lazy val Any_typeCast: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOfPM, _.paramRefs(0), Final | Synthetic | Artifact | StableRealizable)
272-
// generated by pattern matcher, eliminated by erasure
272+
// generated by pattern matcher and exlicit nulls, eliminated by erasure
273273

274274
/** def getClass[A >: this.type](): Class[? <: A] */
275275
@tu lazy val Any_getClass: TermSymbol =

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

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SymDenotations._
1717
import Decorators._
1818
import Denotations._
1919
import Periods._
20+
import CheckRealizable._
2021
import util.Stats._
2122
import util.SimpleIdentitySet
2223
import reporting.diagnostic.Message
@@ -163,6 +164,9 @@ object Types {
163164
case tp: RefinedOrRecType => tp.parent.isStable
164165
case tp: ExprType => tp.resultType.isStable
165166
case tp: AnnotatedType => tp.parent.isStable
167+
case tp: AndType =>
168+
tp.tp1.isStable && (realizability(tp.tp2) eq Realizable) ||
169+
tp.tp2.isStable && (realizability(tp.tp1) eq Realizable)
166170
case _ => false
167171
}
168172

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Names._
1212
import StdNames._
1313
import Contexts._
1414
import Nullables.{CompareNull, TrackedRef}
15+
import NullOpsDecorator._
1516

1617
object ConstFold {
1718

@@ -20,10 +21,6 @@ object ConstFold {
2021
/** If tree is a constant operation, replace with result. */
2122
def apply[T <: Tree](tree: T)(implicit ctx: Context): T = finish(tree) {
2223
tree match {
23-
case CompareNull(TrackedRef(ref), testEqual)
24-
if ctx.settings.YexplicitNulls.value && ctx.notNullInfos.impliesNotNull(ref) =>
25-
// TODO maybe drop once we have general Nullability?
26-
Constant(!testEqual)
2724
case Apply(Select(xt, op), yt :: Nil) =>
2825
xt.tpe.widenTermRefExpr.normalized match
2926
case ConstantType(x) =>

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

+53-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import util.Property
1010
import Names.Name
1111
import util.Spans.Span
1212
import Flags.Mutable
13+
import NullOpsDecorator._
1314
import collection.mutable
1415

1516
/** Operations for implementing a flow analysis for nullability */
@@ -104,13 +105,57 @@ object Nullables with
104105
* This is the case if the reference is a path to an immutable val, or if it refers
105106
* to a local mutable variable where all assignments to the variable are _reachable_
106107
* (in the sense of how it is defined in assignmentSpans).
108+
*
109+
* When dealing with local mutable variables, there are two questions:
110+
*
111+
* 1. Whether to track a local mutable variable during flow typing.
112+
* We track a local mutable variable iff the variable is not assigned in a closure.
113+
* For example, in the following code `x` is assigned to by the closure `y`, so we do not
114+
* do flow typing on `x`.
115+
* ```scala
116+
* var x: String|Null = ???
117+
* def y = {
118+
* x = null
119+
* }
120+
* if (x != null) {
121+
* // y can be called here, which break the fact
122+
* val a: String = x // error: x is captured and mutated by the closure, not tackable
123+
* }
124+
* ```
125+
*
126+
* 2. Whether to generate and use flow typing on a specific _use_ of a local mutable variable.
127+
* We only want to do flow typing on a use that belongs to the same method as the definition
128+
* of the local variable.
129+
* For example, in the following code, even `x` is not assigned to by a closure, but we can only
130+
* use flow typing in one of the occurrences (because the other occurrence happens within a nested
131+
* closure).
132+
* ```scala
133+
* var x: String|Null = ???
134+
* def y = {
135+
* if (x != null) {
136+
* // not safe to use the fact (x != null) here
137+
* // since y can be executed at the same time as the outer block
138+
* val _: String = x
139+
* }
140+
* }
141+
* if (x != null) {
142+
* val a: String = x // ok to use the fact here
143+
* x = null
144+
* }
145+
* ```
146+
*
147+
* See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`.
107148
*/
108149
def isTracked(ref: TermRef)(given Context) =
109150
ref.isStable
110151
|| { val sym = ref.symbol
111152
sym.is(Mutable)
112153
&& sym.owner.isTerm
113-
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod
154+
&& ( sym.owner == curCtx.owner
155+
|| !curCtx.owner.is(Flags.Lazy) // not at the rhs of lazy ValDef
156+
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod // not in different methods
157+
// TODO: need to check by-name paramter
158+
)
114159
&& sym.span.exists
115160
&& curCtx.compilationUnit != null // could be null under -Ytest-pickler
116161
&& curCtx.compilationUnit.assignmentSpans.contains(sym.span.start)
@@ -254,7 +299,13 @@ object Nullables with
254299
given assignOps: (tree: Assign)
255300
def computeAssignNullable()(given Context): tree.type = tree.lhs match
256301
case TrackedRef(ref) =>
257-
tree.withNotNullInfo(NotNullInfo(Set(), Set(ref))) // TODO: refine with nullability type info
302+
val rhstp = tree.rhs.typeOpt
303+
if (rhstp.isNullType || (curCtx.explicitNulls && rhstp.isNullableUnion))
304+
// If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the
305+
// lhs variable is no longer trackable. We don't need to check whether the type `T`
306+
// is correct here, as typer will check it.
307+
tree.withNotNullInfo(NotNullInfo(Set(), Set(ref)))
308+
else tree
258309
case _ => tree
259310

260311
private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!)

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

+42-19
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import transform.SymUtils._
4141
import transform.TypeUtils._
4242
import reporting.trace
4343
import Nullables.{NotNullInfo, given}
44+
import NullOpsDecorator._
4445

4546
object Typer {
4647

@@ -347,6 +348,16 @@ class Typer extends Namer
347348
findRefRecur(NoType, BindingPrec.NothingBound, NoContext)
348349
}
349350

351+
// If `tree`'s type is a `TermRef` identified by flow typing to be non-null, then
352+
// cast away `tree`s nullability. Otherwise, `tree` remains unchanged.
353+
def toNotNullTermRef(tree: Tree, pt: Type)(implicit ctx: Context): Tree = tree.tpe match
354+
case ref @ OrNull(tpnn) : TermRef
355+
if pt != AssignProto && // Ensure it is not the lhs of Assign
356+
ctx.notNullInfos.impliesNotNull(ref) =>
357+
tree.select(defn.Any_typeCast).appliedToType(AndType(ref, tpnn))
358+
case _ =>
359+
tree
360+
350361
/** Attribute an identifier consisting of a simple name or wildcard
351362
*
352363
* @param tree The tree representing the identifier.
@@ -417,7 +428,9 @@ class Typer extends Namer
417428
tree.withType(ownType)
418429
}
419430

420-
checkStableIdentPattern(tree1, pt)
431+
val tree2 = toNotNullTermRef(tree1, pt)
432+
433+
checkStableIdentPattern(tree2, pt)
421434
}
422435

423436
/** Check that a stable identifier pattern is indeed stable (SLS 8.1.5)
@@ -442,8 +455,11 @@ class Typer extends Namer
442455
case qual =>
443456
if (tree.name.isTypeName) checkStable(qual.tpe, qual.sourcePos)
444457
val select = assignType(cpy.Select(tree)(qual, tree.name), qual)
445-
if (select.tpe ne TryDynamicCallType) ConstFold(checkStableIdentPattern(select, pt))
446-
else if (pt.isInstanceOf[FunOrPolyProto] || pt == AssignProto) select
458+
459+
val select1 = toNotNullTermRef(select, pt)
460+
461+
if (select1.tpe ne TryDynamicCallType) ConstFold(checkStableIdentPattern(select1, pt))
462+
else if (pt.isInstanceOf[FunOrPolyProto] || pt == AssignProto) select1
447463
else typedDynamicSelect(tree, Nil, pt)
448464
}
449465

@@ -1554,16 +1570,6 @@ class Typer extends Namer
15541570
typed(annot, defn.AnnotationClass.typeRef)
15551571

15561572
def typedValDef(vdef: untpd.ValDef, sym: Symbol)(implicit ctx: Context): Tree = {
1557-
sym.infoOrCompleter match
1558-
case completer: Namer#Completer
1559-
if completer.creationContext.notNullInfos ne ctx.notNullInfos =>
1560-
// The RHS of a val def should know about not null facts established
1561-
// in preceding statements (unless the ValDef is completed ahead of time,
1562-
// then it is impossible).
1563-
vdef.symbol.info = Completer(completer.original)(
1564-
given completer.creationContext.withNotNullInfos(ctx.notNullInfos))
1565-
case _ =>
1566-
15671573
val ValDef(name, tpt, _) = vdef
15681574
completeAnnotations(vdef, sym)
15691575
if (sym.isOneOf(GivenOrImplicit)) checkImplicitConversionDefOK(sym)
@@ -2216,14 +2222,31 @@ class Typer extends Namer
22162222
case Some(xtree) =>
22172223
traverse(xtree :: rest)
22182224
case none =>
2219-
val defCtx = mdef match
2225+
def defCtx = ctx.withNotNullInfos(initialNotNullInfos)
2226+
val newCtx = if (ctx.owner.isTerm) {
22202227
// Keep preceding not null facts in the current context only if `mdef`
22212228
// cannot be executed out-of-sequence.
2222-
case _: ValDef if !mdef.mods.is(Lazy) && ctx.owner.isTerm =>
2223-
ctx // all preceding statements will have been executed in this case
2224-
case _ =>
2225-
ctx.withNotNullInfos(initialNotNullInfos)
2226-
typed(mdef)(given defCtx) match {
2229+
// We have to check the Completer of symbol befor typedValDef,
2230+
// otherwise the symbol is already completed using creation context.
2231+
mdef.getAttachment(SymOfTree).map(s => (s, s.infoOrCompleter)) match {
2232+
case Some((sym, completer: Namer#Completer)) =>
2233+
if (completer.creationContext.notNullInfos ne ctx.notNullInfos)
2234+
// The RHS of a val def should know about not null facts established
2235+
// in preceding statements (unless the DefTree is completed ahead of time,
2236+
// then it is impossible).
2237+
sym.info = Completer(completer.original)(
2238+
given completer.creationContext.withNotNullInfos(ctx.notNullInfos))
2239+
ctx // all preceding statements will have been executed in this case
2240+
case _ =>
2241+
// If it has been completed, then it must be because there is a forward reference
2242+
// to the definition in the program. Hence, we don't Keep preceding not null facts
2243+
// in the current context.
2244+
defCtx
2245+
}
2246+
}
2247+
else defCtx
2248+
2249+
typed(mdef)(given newCtx) match {
22272250
case mdef1: DefDef if !Inliner.bodyToInline(mdef1.symbol).isEmpty =>
22282251
buf += inlineExpansion(mdef1)
22292252
// replace body with expansion, because it will be used as inlined body

0 commit comments

Comments
 (0)