Skip to content

Commit 66e83b4

Browse files
committed
Address review comments
1 parent 5c65cca commit 66e83b4

File tree

8 files changed

+70
-79
lines changed

8 files changed

+70
-79
lines changed

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

-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ object FlowTyper {
207207
* 1) a return
208208
* 2) an expression of type `Nothing` (in particular, usages of `throw`)
209209
* 3) a block where the last expression is non-local
210-
* 4) nothing else is non-local
211210
*
212211
* So, for example, if we know that `x` must be non-null if `cond` is true, and `else` is non-local,
213212
* then in order for `s2` to execute `cond` must be true. We can thus soundly add `x` to our

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

+38-42
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import dotty.tools.dotc.core.StdNames.{jnme, nme}
66
import dotty.tools.dotc.core.Symbols.{Symbol, defn, _}
77
import dotty.tools.dotc.core.Types.{AndType, AppliedType, LambdaType, MethodType, OrType, PolyType, Type, TypeAlias, TypeMap, TypeParamRef, TypeRef}
88

9-
/** This module defines methods to interpret Java types, which are implicitly nullable,
9+
/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java,
1010
* as Scala types, which are explicitly nullable.
1111
*
12-
* The transformation from Java types to Scala types is (conceptually) a function `n`
13-
* that adheres to the following rules:
12+
* The transformation is (conceptually) a function `n` that adheres to the following rules:
1413
* (1) n(T) = T|JavaNull if T is a reference type
1514
* (2) n(T) = T if T is a value type
1615
* (3) n(C[T]) = C[T]|JavaNull if C is Java-defined
@@ -20,6 +19,15 @@ import dotty.tools.dotc.core.Types.{AndType, AppliedType, LambdaType, MethodType
2019
* (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R
2120
* (8) n(T) = T otherwise
2221
*
22+
* Treatment of generics (rules 3 and 4):
23+
* - if `C` is Java-defined, then `n(C[T]) = C[T]|JavaNull`. That is, we don't recurse
24+
* on the type argument, and only add JavaNull on the outside. This is because
25+
* `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body.
26+
* e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so
27+
* we don't need to write `java.util.List[String|Null]`.
28+
* - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)]|JavaNull`. This is because
29+
* `C` won't be nullified, so we need to indicate that its type argument is nullable.
30+
*
2331
* Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need
2432
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and
2533
* enum instances get special treatment.
@@ -47,30 +55,22 @@ object JavaNullInterop {
4755
assert(ctx.explicitNulls)
4856
assert(sym.is(JavaDefined), "can only nullify java-defined members")
4957

50-
// A list of members that are special-cased.
58+
// A list of "policies" that special-case certain members.
59+
// The policies should be disjoint: we use the first one that is applicable.
5160
val whitelist: Seq[NullifyPolicy] = Seq(
5261
// The `TYPE` field in every class: don't nullify.
53-
NoOpP(_.name == nme.TYPE_),
62+
NoOpPolicy(_.name == nme.TYPE_),
5463
// The `toString` method: don't nullify the return type.
55-
paramsOnlyP(_.name == nme.toString_),
64+
paramsOnlyPolicy(_.name == nme.toString_),
5665
// Constructors: params are nullified, but the result type isn't.
57-
paramsOnlyP(_.isConstructor),
66+
paramsOnlyPolicy(_.isConstructor),
5867
// Java enum instances: don't nullify.
59-
NoOpP(_.is(Flags.JavaEnumValue))
68+
NoOpPolicy(_.is(Flags.JavaEnumValue))
6069
)
6170

62-
val (fromWhitelistTp, handled) = whitelist.foldLeft((tp, false)) {
63-
case (res@(_, true), _) => res
64-
case ((_, false), pol) =>
65-
if (pol.isApplicable(sym)) (pol(tp), true)
66-
else (tp, false)
67-
}
68-
69-
if (handled) {
70-
fromWhitelistTp
71-
} else {
72-
// Default case: nullify everything.
73-
nullifyType(tp)
71+
whitelist.find(_.isApplicable(sym)) match {
72+
case Some(pol) => pol(tp)
73+
case None => nullifyType(tp) // default case: nullify everything
7474
}
7575
}
7676

@@ -83,7 +83,7 @@ object JavaNullInterop {
8383
}
8484

8585
/** A policy that leaves the passed-in type unchanged. */
86-
private case class NoOpP(trigger: Symbol => Boolean) extends NullifyPolicy {
86+
private case class NoOpPolicy(trigger: Symbol => Boolean) extends NullifyPolicy {
8787
override def isApplicable(sym: Symbol): Boolean = trigger(sym)
8888

8989
override def apply(tp: Type): Type = tp
@@ -98,9 +98,9 @@ object JavaNullInterop {
9898
* this applies only at the top level. e.g. suppose we have a Java result type `Array[String]` and `nnRes` is set.
9999
* Scala will see `Array[String|JavaNull]`; the array element type is still nullified.
100100
*/
101-
private case class MethodP(trigger: Symbol => Boolean,
102-
nnParams: Seq[Int],
103-
nnRes: Boolean)(implicit ctx: Context) extends TypeMap with NullifyPolicy {
101+
private case class MethodPolicy(trigger: Symbol => Boolean,
102+
nnParams: Seq[Int],
103+
nnRes: Boolean)(implicit ctx: Context) extends TypeMap with NullifyPolicy {
104104
override def isApplicable(sym: Symbol): Boolean = trigger(sym)
105105

106106
private def spare(tp: Type): Type = {
@@ -125,8 +125,8 @@ object JavaNullInterop {
125125
}
126126

127127
/** A policy that nullifies only method parameters (but not result types). */
128-
private def paramsOnlyP(trigger: Symbol => Boolean)(implicit ctx: Context): MethodP = {
129-
MethodP(trigger, nnParams = Seq.empty, nnRes = true)
128+
private def paramsOnlyPolicy(trigger: Symbol => Boolean)(implicit ctx: Context): MethodPolicy = {
129+
MethodPolicy(trigger, nnParams = Seq.empty, nnRes = true)
130130
}
131131

132132
/** Nullifies a Java type by adding `| JavaNull` in the relevant places. */
@@ -140,7 +140,7 @@ object JavaNullInterop {
140140
* This is needed so that `JavaNullMap(A | B)` gives back `(A | B) | JavaNull`,
141141
* instead of `(A|JavaNull | B|JavaNull) | JavaNull`.
142142
*/
143-
private class JavaNullMap(alreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap {
143+
private class JavaNullMap(var alreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap {
144144
/** Should we nullify `tp` at the outermost level? */
145145
def needsTopLevelNull(tp: Type): Boolean = {
146146
!alreadyNullable && (tp match {
@@ -158,30 +158,26 @@ object JavaNullInterop {
158158
* `java.util.List[String|Null]` contain nullable elements.
159159
*/
160160
def needsNullArgs(tp: AppliedType): Boolean = {
161-
val AppliedType(tycons, _) = tp
162-
tycons.widenDealias match {
163-
case tp: TypeRef if !tp.symbol.is(JavaDefined) => true
164-
case _ => false
165-
}
161+
!tp.classSymbol.is(JavaDefined)
166162
}
167163

168164
override def apply(tp: Type): Type = {
169165
tp match {
166+
case tp: TypeRef if needsTopLevelNull(tp) => tp.toJavaNullableUnion
167+
case appTp @ AppliedType(tycons, targs) =>
168+
val targs2 = if (needsNullArgs(appTp)) targs map this else targs
169+
derivedAppliedType(appTp, tycons, targs2).toJavaNullableUnion
170170
case tp: LambdaType => mapOver(tp)
171171
case tp: TypeAlias => mapOver(tp)
172-
case tp@AndType(tp1, tp2) =>
172+
case tp @ AndType(tp1, tp2) =>
173173
// nullify(A & B) = (nullify(A) & nullify(B)) | JavaNull, but take care not to add
174174
// duplicate `JavaNull`s at the outermost level inside `A` and `B`.
175-
val newMap = new JavaNullMap(alreadyNullable = true)
176-
derivedAndType(tp, newMap(tp1), newMap(tp2)).toJavaNullableUnion
177-
case tp@OrType(tp1, tp2) if !tp.isJavaNullableUnion =>
178-
val newMap = new JavaNullMap(alreadyNullable = true)
179-
derivedOrType(tp, newMap(tp1), newMap(tp2)).toJavaNullableUnion
180-
case tp: TypeRef if needsTopLevelNull(tp) => tp.toJavaNullableUnion
175+
alreadyNullable = true
176+
derivedAndType(tp, this(tp1), this(tp2)).toJavaNullableUnion
177+
case tp @ OrType(tp1, tp2) if !tp.isJavaNullableUnion =>
178+
alreadyNullable = true
179+
derivedOrType(tp, this(tp1), this(tp2)).toJavaNullableUnion
181180
case tp: TypeParamRef if needsTopLevelNull(tp) => tp.toJavaNullableUnion
182-
case appTp@AppliedType(tycons, targs) =>
183-
val targs2 = if (needsNullArgs(appTp)) targs map this else targs
184-
derivedAppliedType(appTp, tycons, targs2).toJavaNullableUnion
185181
case _ => tp
186182
}
187183
}

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -693,11 +693,8 @@ object SymDenotations {
693693

694694
/** Is this symbol a class of which `null` is a value? */
695695
final def isNullableClass(implicit ctx: Context): Boolean = {
696-
if (ctx.explicitNulls && !ctx.phase.erasedTypes) {
697-
symbol == defn.NullClass || symbol == defn.AnyClass
698-
} else {
699-
isNullableClassAfterErasure
700-
}
696+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyClass
697+
else isNullableClassAfterErasure
701698
}
702699

703700
/** Is this symbol a class of which `null` is a value after erasure?

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

+9-11
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,15 @@ object Types {
418418
all.tail.foldLeft(all.head)(OrType.apply)
419419
}
420420

421-
/** Strips types according to `stripPred` from a nullable union. */
422-
private def stripNullWithPred(stripPred: Type => Boolean)(implicit ctx: Context): Type = {
421+
/** Strips `Null` from type unions in this type.
422+
* @param onlyJavaNull if true, we delete only `JavaNull` and not vanilla `Null`.
423+
*/
424+
private def stripNullImpl(onlyJavaNull: Boolean)(implicit ctx: Context): Type = {
423425
assert(ctx.explicitNulls)
424426
def strip(tp: Type, changed: Boolean): (Type, Boolean) = {
425427
tp match {
426428
case OrType(left, right) if right.isNullType =>
427-
if (stripPred(right)) strip(left, changed = true)
429+
if (!onlyJavaNull || right.isJavaNullType) strip(left, changed = true)
428430
else {
429431
val (tp1, changed1) = strip(left, changed)
430432
(OrType(tp1, right), changed1)
@@ -446,13 +448,13 @@ object Types {
446448
*/
447449
def stripNull(implicit ctx: Context): Type = {
448450
assert(ctx.explicitNulls)
449-
stripNullWithPred(_.isNullType)
451+
stripNullImpl(onlyJavaNull = false)
450452
}
451453

452454
/** Like `stripNull`, but removes only the `JavaNull`s. */
453455
def stripJavaNull(implicit ctx: Context): Type = {
454456
assert(ctx.explicitNulls)
455-
stripNullWithPred(_.isJavaNullType)
457+
stripNullImpl(onlyJavaNull = true)
456458
}
457459

458460
/** Collapses all `JavaNull` unions within this type, and not just the outermost ones (as `stripJavaNull` does).
@@ -2028,19 +2030,15 @@ object Types {
20282030
// If the denotation is computed for the first time, or if it's ever updated, make sure
20292031
// that the `info` is non-null.
20302032
d.mapInfo(_.stripNull)
2031-
} else {
2032-
d
2033-
}
2033+
} else d
20342034
// Avoid storing NoDenotations in the cache - we will not be able to recover from
20352035
// them. The situation might arise that a type has NoDenotation in some later
20362036
// phase but a defined denotation earlier (e.g. a TypeRef to an abstract type
20372037
// is undefined after erasure.) We need to be able to do time travel back and
20382038
// forth also in these cases.
20392039
setDenot(d1)
20402040
d1
2041-
} else {
2042-
d
2043-
}
2041+
} else d
20442042
}
20452043

20462044
def fromDesignator = designator match {

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

+1-3
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,7 @@ class ClassfileParser(
270270
if ((denot is Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0)
271271
denot.info = arrayToRepeated(denot.info)
272272

273-
if (ctx.explicitNulls) {
274-
denot.info = JavaNullInterop.nullifyMember(denot.symbol, denot.info)
275-
}
273+
if (ctx.explicitNulls) denot.info = JavaNullInterop.nullifyMember(denot.symbol, denot.info)
276274

277275
// seal java enums
278276
if (isEnum) {

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

+10-12
Original file line numberDiff line numberDiff line change
@@ -837,18 +837,16 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
837837
}
838838

839839
// Separate into a function so we can pass the updated context.
840-
def proc(implicit ctx: Context): tpd.Tree = {
841-
methPart(fun1).tpe match {
842-
case funRef: TermRef =>
843-
val app =
844-
if (proto.allArgTypesAreCurrent())
845-
new ApplyToTyped(tree, fun1, funRef, proto.unforcedTypedArgs, pt)
846-
else
847-
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(argCtx(tree))
848-
convertNewGenericArray(app.result)
849-
case _ =>
850-
handleUnexpectedFunType(tree, fun1)
851-
}
840+
def proc(implicit ctx: Context): tpd.Tree = methPart(fun1).tpe match {
841+
case funRef: TermRef =>
842+
val app =
843+
if (proto.allArgTypesAreCurrent())
844+
new ApplyToTyped(tree, fun1, funRef, proto.unforcedTypedArgs, pt)
845+
else
846+
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(argCtx(tree))
847+
convertNewGenericArray(app.result)
848+
case _ =>
849+
handleUnexpectedFunType(tree, fun1)
852850
}
853851

854852
proc(ctx1)

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,7 @@ class Typer extends Namer
418418
} else
419419
errorType(new MissingIdent(tree, kind, name.show), tree.sourcePos)
420420

421-
val ownType1 = if (ctx.explicitNulls) {
422-
FlowTyper.refineType(ownType)
423-
} else {
424-
ownType
425-
}
421+
val ownType1 = if (ctx.explicitNulls) FlowTyper.refineType(ownType) else ownType
426422

427423
val tree1 = ownType1 match {
428424
case ownType1: NamedType if !prefixIsElidable(ownType1) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Test that `toString` has been special-cased to
2+
// return a non-nullable value.
3+
4+
class Foo {
5+
val x: java.lang.Integer = 42
6+
val y: String = x.toString // would fail if toString returns nullable value
7+
val y2 = x.toString // test interaction with type inference
8+
val z: String = y2
9+
}

0 commit comments

Comments
 (0)