Skip to content

Commit 213fa9b

Browse files
evangirardinolhotak
authored andcommitted
Add experimental flexible types feature on top of explicit nulls
Enabled by -Yflexible-types with -Yexplicit-nulls. A flexible type T! is a non-denotable type such that T <: T! <: T|Null and T|Null <: T! <: T. Here we patch return types and parameter types of Java methods and fields to use flexible types. This is unsound and kills subtyping transitivity but makes interop with Java play more nicely with the explicit nulls experimental feature (i.e. fewer nullability casts). Also adds a few tests for flexible types, mostly lifted from the explicit nulls tests.
1 parent 4367b20 commit 213fa9b

File tree

4 files changed

+108
-46
lines changed

4 files changed

+108
-46
lines changed

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

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2577,53 +2577,73 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
25772577
/** Try to distribute `&` inside type, detect and handle conflicts
25782578
* @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before
25792579
*/
2580-
private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match {
2581-
case tp1 @ AppliedType(tycon1, args1) =>
2582-
tp2 match {
2583-
case AppliedType(tycon2, args2)
2584-
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2585-
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2586-
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2587-
else NoType
2588-
case _ =>
2589-
NoType
2590-
}
2591-
case tp1: RefinedType =>
2592-
// opportunistically merge same-named refinements
2593-
// this does not change anything semantically (i.e. merging or not merging
2594-
// gives =:= types), but it keeps the type smaller.
2595-
tp2 match {
2596-
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2597-
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2598-
if jointInfo.exists then
2599-
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2600-
else
2580+
private def distributeAnd(tp1: Type, tp2: Type): Type = {
2581+
var ft1 = false
2582+
var ft2 = false
2583+
def recur(tp1: Type, tp2: Type): Type = tp1 match {
2584+
case tp1 @ FlexibleType(tp) =>
2585+
// Hack -- doesn't generalise to other intersection/union types
2586+
// but covers a common special case for pattern matching
2587+
ft1 = true
2588+
recur(tp, tp2)
2589+
case tp1 @ AppliedType(tycon1, args1) =>
2590+
tp2 match {
2591+
case AppliedType(tycon2, args2)
2592+
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2593+
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2594+
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2595+
else {
2596+
NoType
2597+
}
2598+
case FlexibleType(tp) =>
2599+
// Hack from above
2600+
ft2 = true
2601+
recur(tp1, tp)
2602+
case _ =>
26012603
NoType
2602-
case _ =>
2603-
NoType
2604-
}
2605-
case tp1: RecType =>
2606-
tp1.rebind(distributeAnd(tp1.parent, tp2))
2607-
case ExprType(rt1) =>
2608-
tp2 match {
2609-
case ExprType(rt2) =>
2610-
ExprType(rt1 & rt2)
2611-
case _ =>
2612-
NoType
2613-
}
2614-
case tp1: TypeVar if tp1.isInstantiated =>
2615-
tp1.underlying & tp2
2616-
case CapturingType(parent1, refs1) =>
2617-
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2618-
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2619-
then
2620-
parent1 & tp2
2621-
else
2622-
tp1.derivedCapturingType(parent1 & tp2, refs1)
2623-
case tp1: AnnotatedType if !tp1.isRefining =>
2624-
tp1.underlying & tp2
2625-
case _ =>
2626-
NoType
2604+
}
2605+
2606+
// if result exists and is not notype, maybe wrap result in flex based on whether seen flex on both sides
2607+
case tp1: RefinedType =>
2608+
// opportunistically merge same-named refinements
2609+
// this does not change anything semantically (i.e. merging or not merging
2610+
// gives =:= types), but it keeps the type smaller.
2611+
tp2 match {
2612+
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2613+
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2614+
if jointInfo.exists then
2615+
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2616+
else
2617+
NoType
2618+
case _ =>
2619+
NoType
2620+
}
2621+
case tp1: RecType =>
2622+
tp1.rebind(recur(tp1.parent, tp2))
2623+
case ExprType(rt1) =>
2624+
tp2 match {
2625+
case ExprType(rt2) =>
2626+
ExprType(rt1 & rt2)
2627+
case _ =>
2628+
NoType
2629+
}
2630+
case tp1: TypeVar if tp1.isInstantiated =>
2631+
tp1.underlying & tp2
2632+
case CapturingType(parent1, refs1) =>
2633+
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2634+
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2635+
then
2636+
parent1 & tp2
2637+
else
2638+
tp1.derivedCapturingType(parent1 & tp2, refs1)
2639+
case tp1: AnnotatedType if !tp1.isRefining =>
2640+
tp1.underlying & tp2
2641+
case _ =>
2642+
NoType
2643+
}
2644+
// if flex on both sides, return flex type
2645+
val ret = recur(tp1, tp2)
2646+
if (ft1 && ft2) then FlexibleType(ret) else ret
26272647
}
26282648

26292649
/** Try to distribute `|` inside type, detect and handle conflicts

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ object TypeErasure {
561561
case tp: TypeProxy => hasStableErasure(tp.translucentSuperType)
562562
case tp: AndType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2)
563563
case tp: OrType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2)
564+
case _: FlexibleType => false
564565
case _ => false
565566
}
566567

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<module type="JAVA_MODULE" version="4">
3+
<component name="NewModuleRootManager" inherit-compiler-output="true">
4+
<exclude-output />
5+
<content url="file://$MODULE_DIR$">
6+
<sourceFolder url="file://$MODULE_DIR$/neg" isTestSource="false" />
7+
</content>
8+
<orderEntry type="inheritedJdk" />
9+
<orderEntry type="sourceFolder" forTests="false" />
10+
</component>
11+
</module>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
class Foo {
3+
def err(msg: String): Nothing = {
4+
throw new RuntimeException("Hello")
5+
}
6+
def retTypeNothing(): String = {
7+
val y: String|Null = ???
8+
if (y == null) err("y is null!")
9+
y
10+
}
11+
}
12+
*/
13+
14+
15+
16+
@main def main() = {
17+
val i : Integer = new Integer(3) // Constructor with non-ref arg
18+
val s1 : String | Null = new String("abc") // Constructor with ref arg
19+
val s2 : String = new String("abc") // Constructor with ref arg, not null
20+
val s3 = s1.nn.substring(0,1).substring(0,1)
21+
val s4 = s2.substring(0,1).substring(0,1)
22+
val s5 = s4.startsWith(s4)
23+
// s1.substring(0,1) // error
24+
val j : J = new J("")
25+
println(s4)
26+
//val f : Foo = new Foo("x")
27+
//f.err("Hello")
28+
//val l : List[String] = Java.returnsNull();
29+
//val j : J = new J
30+
}

0 commit comments

Comments
 (0)