Skip to content

Commit 9e88707

Browse files
authored
Apply flexible types to files compiled without explicit nulls (#23386)
A fixed version of #22473 This PR wraps the types of symbols from files compiled without explicit nulls in flexible types. This allows for interop between multiple files in cases where ```scala class Unsafe_1 { def foo(s: String): String = { if (s == null) then "nullString" else s } } ``` compiled without explicit nulls can still be used in ```scala def Flexible_2() = val s2: String | Null = "foo" val unsafe = new Unsafe_1() val s: String = unsafe.foo(s2) unsafe.foo("") unsafe.foo(null) ``` whereas the argument would have been a strictly non-null String, because of the flexible type, the function call is now permitted. Some explicit nulls neg tests have been excluded under the scala2 library tasty tests due to technical reasons, for example: ```scala class S { given Conversion[String, Array[String]] = _ => ??? def f = { val s: String | Null = ??? val x: String = s // error val xl = s.length // error val xs: Array[String | Null] | Null = s // error } } ``` `val xl = s.length` will not give an error. This is because the .length function is from the WrappedString.scala library. After desugaring, it will become `wrapString(s).length`. Since the scala2 library is compiled without explicit nulls, the types are flexified.`wrapString` will have argument type FlexibleType(String) and return type FlexibleType(String), therefore it will not give an error. Hence we need to exclude it from the tasty tests. [test_scala2_library_tasty]
2 parents 0a7f843 + 5f8c59a commit 9e88707

File tree

11 files changed

+277
-28
lines changed

11 files changed

+277
-28
lines changed

compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala renamed to compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import dotty.tools.dotc.core.Decorators.i
3535
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and
3636
* enum instances get special treatment.
3737
*/
38-
object JavaNullInterop {
38+
object ImplicitNullInterop {
3939

4040
/** Transforms the type `tp` of Java member `sym` to be explicitly nullable.
4141
* `tp` is needed because the type inside `sym` might not be set when this method is called.
@@ -55,11 +55,11 @@ object JavaNullInterop {
5555
*/
5656
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"){
5757
assert(ctx.explicitNulls)
58-
assert(sym.is(JavaDefined), "can only nullify java-defined members")
5958

6059
// Some special cases when nullifying the type
61-
if isEnumValueDef || sym.name == nme.TYPE_ then
62-
// Don't nullify the `TYPE` field in every class and Java enum instances
60+
if isEnumValueDef || sym.name == nme.TYPE_ // Don't nullify the `TYPE` field in every class and Java enum instances
61+
|| sym.is(Flags.ModuleVal) // Don't nullify Modules
62+
then
6363
tp
6464
else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then
6565
// Don't nullify the return type of the `toString` method.
@@ -80,14 +80,14 @@ object JavaNullInterop {
8080
* but the result type is not nullable.
8181
*/
8282
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
83-
new JavaNullMap(outermostLevelAlreadyNullable = true)(tp)
83+
new ImplicitNullMap(outermostLevelAlreadyNullable = true)(tp)
8484

85-
/** Nullifies a Java type by adding `| Null` in the relevant places. */
85+
/** Nullifies a type by adding `| Null` in the relevant places. */
8686
private def nullifyType(tp: Type)(using Context): Type =
87-
new JavaNullMap(outermostLevelAlreadyNullable = false)(tp)
87+
new ImplicitNullMap(outermostLevelAlreadyNullable = false)(tp)
8888

89-
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null`
90-
* in the right places to make the nulls explicit in Scala.
89+
/** A type map that implements the nullification function on types. Given a Java-sourced type or an
90+
* implicitly null type, this adds `| Null` in the right places to make the nulls explicit.
9191
*
9292
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level.
9393
* For example, `Array[String] | Null` is already nullable at the
@@ -97,26 +97,32 @@ object JavaNullInterop {
9797
* This is useful for e.g. constructors, and also so that `A & B` is nullified
9898
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
9999
*/
100-
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
100+
private class ImplicitNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
101101
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp)
102102

103103
/** Should we nullify `tp` at the outermost level? */
104104
def needsNull(tp: Type): Boolean =
105105
if outermostLevelAlreadyNullable then false
106106
else tp match
107-
case tp: TypeRef if
107+
case tp: TypeRef if !tp.hasSimpleKind
108108
// We don't modify value types because they're non-nullable even in Java.
109-
tp.symbol.isValueClass
109+
|| tp.symbol.isValueClass
110110
// We don't modify unit types.
111111
|| tp.isRef(defn.UnitClass)
112112
// We don't modify `Any` because it's already nullable.
113-
|| tp.isRef(defn.AnyClass)
114-
// We don't nullify Java varargs at the top level.
115-
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
116-
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
117-
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
118-
// and not a `null` array.
119-
|| !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false
113+
|| tp.isRef(defn.AnyClass) => false
114+
case _ => true
115+
116+
// We don't nullify Java varargs at the top level.
117+
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
118+
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
119+
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
120+
// and not a `null` array.
121+
def tyconNeedsNull(tp: Type): Boolean =
122+
if outermostLevelAlreadyNullable then false
123+
else tp match
124+
case tp: TypeRef
125+
if !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false
120126
case _ => true
121127

122128
override def apply(tp: Type): Type = tp match {
@@ -130,7 +136,7 @@ object JavaNullInterop {
130136
val targs2 = targs map this
131137
outermostLevelAlreadyNullable = oldOutermostNullable
132138
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
133-
if needsNull(tycon) then nullify(appTp2) else appTp2
139+
if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2
134140
case ptp: PolyType =>
135141
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
136142
case mtp: MethodType =>
@@ -140,6 +146,7 @@ object JavaNullInterop {
140146
outermostLevelAlreadyNullable = oldOutermostNullable
141147
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
142148
case tp: TypeAlias => mapOver(tp)
149+
case tp: TypeBounds => mapOver(tp)
143150
case tp: AndType =>
144151
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
145152
// duplicate `Null`s at the outermost level inside `A` and `B`.
@@ -149,6 +156,14 @@ object JavaNullInterop {
149156
// In all other cases, return the type unchanged.
150157
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
151158
// type of a final non-nullable field.
159+
case tp: ExprType => mapOver(tp)
160+
case tp: AnnotatedType => mapOver(tp)
161+
case tp: OrType =>
162+
outermostLevelAlreadyNullable = true
163+
nullify(derivedOrType(tp, this(tp.tp1), this(tp.tp2)))
164+
case tp: RefinedType =>
165+
outermostLevelAlreadyNullable = true
166+
nullify(mapOver(tp))
152167
case _ => tp
153168
}
154169
}

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,6 +1888,8 @@ object Types extends TypeUtils {
18881888
t
18891889
case t @ SAMType(_, _) =>
18901890
t
1891+
case ft: FlexibleType =>
1892+
ft.underlying.findFunctionType
18911893
case _ =>
18921894
NoType
18931895

@@ -3428,15 +3430,17 @@ object Types extends TypeUtils {
34283430
override def underlying(using Context): Type = hi
34293431

34303432
def derivedFlexibleType(hi: Type)(using Context): Type =
3431-
if hi eq this.hi then this else FlexibleType(hi)
3433+
if hi eq this.hi then this else FlexibleType.make(hi)
34323434

34333435
override def computeHash(bs: Binders): Int = doHash(bs, hi)
34343436

34353437
override final def baseClasses(using Context): List[ClassSymbol] = hi.baseClasses
34363438
}
34373439

34383440
object FlexibleType {
3439-
def apply(tp: Type)(using Context): FlexibleType = tp match {
3441+
def apply(tp: Type)(using Context): FlexibleType =
3442+
assert(tp.isValueType, s"Should not flexify ${tp}")
3443+
tp match {
34403444
case ft: FlexibleType => ft
34413445
case _ =>
34423446
// val tp1 = tp.stripNull()
@@ -3456,6 +3460,15 @@ object Types extends TypeUtils {
34563460
// rule.
34573461
FlexibleType(OrNull(tp), tp)
34583462
}
3463+
3464+
def make(tp: Type)(using Context): Type =
3465+
tp match
3466+
case _: FlexibleType => tp
3467+
case TypeBounds(lo, hi) => TypeBounds(FlexibleType.make(lo), FlexibleType.make(hi))
3468+
case wt: WildcardType => wt.optBounds match
3469+
case tb: TypeBounds => WildcardType(FlexibleType.make(tb).asInstanceOf[TypeBounds])
3470+
case _ => wt
3471+
case other => FlexibleType(tp)
34593472
}
34603473

34613474
// --- AndType/OrType ---------------------------------------------------------------

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ class ClassfileParser(
519519
denot.info = translateTempPoly(attrCompleter.complete(denot.info, isVarargs))
520520
if (isConstructor) normalizeConstructorInfo()
521521

522-
if (ctx.explicitNulls) denot.info = JavaNullInterop.nullifyMember(denot.symbol, denot.info, isEnum)
522+
if (ctx.explicitNulls) denot.info = ImplicitNullInterop.nullifyMember(denot.symbol, denot.info, isEnum)
523523

524524
// seal java enums
525525
if (isEnum) {

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,12 @@ class TreeUnpickler(reader: TastyReader,
921921

922922
def ta = ctx.typeAssigner
923923

924+
// If explicit nulls is enabled, and the source file did not have explicit
925+
// nulls enabled, nullify the member to allow for compatibility.
926+
def nullify(sym: Symbol) =
927+
if (ctx.explicitNulls && ctx.flexibleTypes && !explicitNulls) then
928+
sym.info = ImplicitNullInterop.nullifyMember(sym, sym.info, sym.is(Enum))
929+
924930
val name = readName()
925931
pickling.println(s"reading def of $name at $start")
926932
val tree: MemberDef = tag match {
@@ -937,10 +943,12 @@ class TreeUnpickler(reader: TastyReader,
937943
else
938944
tpt.tpe
939945
sym.info = methodType(paramss, resType)
946+
nullify(sym)
940947
DefDef(paramDefss, tpt)
941948
case VALDEF =>
942949
val tpt = readTpt()(using localCtx)
943950
sym.info = tpt.tpe.suppressIntoIfParam(sym)
951+
nullify(sym)
944952
ValDef(tpt)
945953
case TYPEDEF | TYPEPARAM =>
946954
if (sym.isClass) {
@@ -978,6 +986,9 @@ class TreeUnpickler(reader: TastyReader,
978986
sym.typeRef.recomputeDenot() // make sure we see the new bounds from now on
979987
else
980988
sym.info = info
989+
if (tag == TYPEPARAM) {
990+
nullify(sym)
991+
}
981992

982993
sym.resetFlag(Provisional)
983994
TypeDef(rhs)
@@ -986,6 +997,7 @@ class TreeUnpickler(reader: TastyReader,
986997
val tpt = readTpt()(using localCtx)
987998
assert(nothingButMods(end))
988999
sym.info = tpt.tpe.suppressIntoIfParam(sym)
1000+
nullify(sym)
9891001
ValDef(tpt)
9901002
}
9911003
goto(end)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1921,7 +1921,7 @@ class Namer { typer: Typer =>
19211921
sym.resetFlag(Lazy)
19221922
case _ =>
19231923
if (ctx.explicitNulls && mdef.mods.is(JavaDefined))
1924-
JavaNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue))
1924+
ImplicitNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue))
19251925
else mbrTpe
19261926
}
19271927

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
byname-nullables.scala # identity() flexified
2+
varargs.scala # Array type flexified
3+
flow-conservative.scala # .length flexified
4+
nn-basic.scala # .length flexified but trim rejected
5+
i21380c.scala # .length flexified but replaceAll rejected
6+
unsafe-scope.scala # .length flexified
7+
i17467.scala # Singleton type flexified
8+
from-nullable.scala # Option argument flexified
9+
flow-in-block.scala # .length flexified
10+
array.scala # Type arugment of Array flexified
11+
flow-forward-ref.scala # .length flexified, forward reference error
12+
flow-implicitly.scala # Singleton type flexified
13+
nn.scala # Flexified elided error [!]
14+
flow-basic.scala # .length flexified
15+
16+
unsafe-cast.scala # Array type flexified
17+
unsafe-extensions.scala # Function arguments flexified

compiler/test/dotty/tools/TestSources.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ object TestSources {
4848

4949
def negScala2LibraryTastyExcludelistFile: String = "compiler/test/dotc/neg-scala2-library-tasty.excludelist"
5050
def negInitGlobalScala2LibraryTastyExcludelistFile: String = "compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist"
51+
def negExplicitNullsScala2LibraryTastyExcludelistFile: String = "compiler/test/dotc/neg-explicit-nulls-scala2-library-tasty.excludelist"
5152

5253
def negScala2LibraryTastyExcludelisted: List[String] =
5354
if Properties.usingScalaLibraryTasty then loadList(negScala2LibraryTastyExcludelistFile)
5455
else Nil
5556
def negInitGlobalScala2LibraryTastyExcludelisted: List[String] =
5657
if Properties.usingScalaLibraryTasty then loadList(negInitGlobalScala2LibraryTastyExcludelistFile)
5758
else Nil
59+
def negExplicitNullsScala2LibraryTastyExcludelisted: List[String] =
60+
if Properties.usingScalaLibraryTasty then loadList(negExplicitNullsScala2LibraryTastyExcludelistFile)
61+
else Nil
5862

5963
// patmat tests lists
6064

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@ class CompilationTests {
208208
@Test def explicitNullsNeg: Unit = {
209209
implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg")
210210
aggregateTests(
211-
compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions),
211+
compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions, FileFilter.exclude(TestSources.negExplicitNullsScala2LibraryTastyExcludelisted)),
212212
compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions and "-Yno-flexible-types"),
213-
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-Yno-flexible-types"),
213+
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-Yno-flexible-types", FileFilter.exclude(TestSources.negExplicitNullsScala2LibraryTastyExcludelisted)),
214214
)
215215
}.checkExpectedErrors()
216216

@@ -220,8 +220,18 @@ class CompilationTests {
220220
compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions),
221221
compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions),
222222
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls" and "-Yno-flexible-types"),
223-
)
224-
}.checkCompile()
223+
).checkCompile()
224+
225+
locally {
226+
val tests = List(
227+
compileFile("tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"),
228+
compileFile("tests/explicit-nulls/flexible-unpickle/Flexible_2.scala", explicitNullsOptions.withClasspath(
229+
defaultOutputDir + testGroup + "/Unsafe_1/flexible-unpickle/Unsafe_1")),
230+
).map(_.keepOutput.checkCompile())
231+
232+
tests.foreach(_.delete())
233+
}
234+
}
225235

226236
@Test def explicitNullsWarn: Unit = {
227237
implicit val testGroup: TestGroup = TestGroup("explicitNullsWarn")
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import unsafeNulls.Foo.*
2+
import unsafeNulls.Unsafe_1
3+
import unsafeNulls.{A, B, C, F, G, H, I, J, L, M}
4+
import scala.reflect.Selectable.reflectiveSelectable
5+
6+
class Inherit_1 extends Unsafe_1 {
7+
override def foo(s: String): String = s
8+
override def bar[T >: String](s: T): T = s
9+
override def bar2[T >: String | Null](s: T): T = s
10+
override def bar3[T <: Function1[String,String]](g: T) = g
11+
override def bar4[HK[_]](i: String | Null): HK[String | Null] = ???
12+
}
13+
14+
class Inherit_2 extends Unsafe_1 {
15+
override def foo(s: String | Null): String | Null = null
16+
override def bar[T >: String](s: T | Null): T | Null = s
17+
override def bar2[T >: String](s: T): T = s
18+
override def bar3[T <: Function1[(String|Null),(String|Null)]](g: T) = g
19+
override def bar4[HK[_]](i: String): HK[String] = ???
20+
}
21+
22+
class Inherit_3 extends Unsafe_1 {
23+
override def foo(s: String): String | Null = null
24+
override def bar[T >: String](s: T): T | Null = s
25+
}
26+
27+
class Inherit_4 extends Unsafe_1 {
28+
override def foo(s: String | Null): String = "non-null string"
29+
override def bar[T >: String](s: T | Null): T = "non-null string"
30+
}
31+
32+
case class cc()
33+
34+
class K(val b: String) extends J(b) {
35+
}
36+
37+
@main
38+
def Flexible_2() =
39+
val s2: String | Null = "foo"
40+
val unsafe = new Unsafe_1()
41+
val s: String = unsafe.foo(s2)
42+
unsafe.foo("")
43+
unsafe.foo(null)
44+
45+
46+
val a = refinement.b
47+
refinement.b = null
48+
val refinement2: Unsafe_1 { var b: String } = refinement
49+
refinement = null
50+
51+
val singletonbar: bar.type = singleton
52+
53+
val extension: String = intersection.reverse
54+
55+
val stringA: String = intersection.stringA
56+
val stringB: String = intersection.stringB
57+
intersection.stringA = null
58+
intersection.stringB = null
59+
60+
val intersection2: A & B = intersection
61+
intersection = null
62+
63+
val stringC: String = union.stringC
64+
union.stringC = null
65+
66+
val union2: A | B = union
67+
union = null
68+
69+
val constructorTest = new Unsafe_1(null)
70+
val member: String = constructorTest.member
71+
constructorTest.member = null
72+
73+
bar match {
74+
case str @ null: String => ()
75+
case other => ()
76+
}
77+
78+
val f = new F(null, G(12))
79+
val F(x, y) = f
80+
81+
val g: (List[F] | String | List[Int]) = F.many
82+
F.many = null :: null :: Nil
83+
F.many = null
84+
85+
val h: H { val s: String } = new H { override val s: String = "foo" }
86+
87+
val jBox: I[J] = new I(new J(null))
88+
val kBox: I[K] = new I(new K("foo"))
89+
90+
val box: I[J] = kBox
91+
92+
val jBox2: L[J] = new L[J](j => ())
93+
val kBox2: L[K] = new L[K](k => ())
94+
95+
val box2: L[K] = jBox2
96+
val box3: I[J | Null] = box
97+
98+
val m: String = M.test(null)
99+

0 commit comments

Comments
 (0)