Skip to content

Commit e69a23e

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 83d60b7 commit e69a23e

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
@@ -2594,53 +2594,73 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
25942594
/** Try to distribute `&` inside type, detect and handle conflicts
25952595
* @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before
25962596
*/
2597-
private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match {
2598-
case tp1 @ AppliedType(tycon1, args1) =>
2599-
tp2 match {
2600-
case AppliedType(tycon2, args2)
2601-
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2602-
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2603-
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2604-
else NoType
2605-
case _ =>
2606-
NoType
2607-
}
2608-
case tp1: RefinedType =>
2609-
// opportunistically merge same-named refinements
2610-
// this does not change anything semantically (i.e. merging or not merging
2611-
// gives =:= types), but it keeps the type smaller.
2612-
tp2 match {
2613-
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2614-
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2615-
if jointInfo.exists then
2616-
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2617-
else
2597+
private def distributeAnd(tp1: Type, tp2: Type): Type = {
2598+
var ft1 = false
2599+
var ft2 = false
2600+
def recur(tp1: Type, tp2: Type): Type = tp1 match {
2601+
case tp1 @ FlexibleType(tp) =>
2602+
// Hack -- doesn't generalise to other intersection/union types
2603+
// but covers a common special case for pattern matching
2604+
ft1 = true
2605+
recur(tp, tp2)
2606+
case tp1 @ AppliedType(tycon1, args1) =>
2607+
tp2 match {
2608+
case AppliedType(tycon2, args2)
2609+
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2610+
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2611+
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2612+
else {
2613+
NoType
2614+
}
2615+
case FlexibleType(tp) =>
2616+
// Hack from above
2617+
ft2 = true
2618+
recur(tp1, tp)
2619+
case _ =>
26182620
NoType
2619-
case _ =>
2620-
NoType
2621-
}
2622-
case tp1: RecType =>
2623-
tp1.rebind(distributeAnd(tp1.parent, tp2))
2624-
case ExprType(rt1) =>
2625-
tp2 match {
2626-
case ExprType(rt2) =>
2627-
ExprType(rt1 & rt2)
2628-
case _ =>
2629-
NoType
2630-
}
2631-
case tp1: TypeVar if tp1.isInstantiated =>
2632-
tp1.underlying & tp2
2633-
case CapturingType(parent1, refs1) =>
2634-
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2635-
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2636-
then
2637-
parent1 & tp2
2638-
else
2639-
tp1.derivedCapturingType(parent1 & tp2, refs1)
2640-
case tp1: AnnotatedType if !tp1.isRefining =>
2641-
tp1.underlying & tp2
2642-
case _ =>
2643-
NoType
2621+
}
2622+
2623+
// if result exists and is not notype, maybe wrap result in flex based on whether seen flex on both sides
2624+
case tp1: RefinedType =>
2625+
// opportunistically merge same-named refinements
2626+
// this does not change anything semantically (i.e. merging or not merging
2627+
// gives =:= types), but it keeps the type smaller.
2628+
tp2 match {
2629+
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2630+
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2631+
if jointInfo.exists then
2632+
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2633+
else
2634+
NoType
2635+
case _ =>
2636+
NoType
2637+
}
2638+
case tp1: RecType =>
2639+
tp1.rebind(recur(tp1.parent, tp2))
2640+
case ExprType(rt1) =>
2641+
tp2 match {
2642+
case ExprType(rt2) =>
2643+
ExprType(rt1 & rt2)
2644+
case _ =>
2645+
NoType
2646+
}
2647+
case tp1: TypeVar if tp1.isInstantiated =>
2648+
tp1.underlying & tp2
2649+
case CapturingType(parent1, refs1) =>
2650+
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2651+
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2652+
then
2653+
parent1 & tp2
2654+
else
2655+
tp1.derivedCapturingType(parent1 & tp2, refs1)
2656+
case tp1: AnnotatedType if !tp1.isRefining =>
2657+
tp1.underlying & tp2
2658+
case _ =>
2659+
NoType
2660+
}
2661+
// if flex on both sides, return flex type
2662+
val ret = recur(tp1, tp2)
2663+
if (ft1 && ft2) then FlexibleType(ret) else ret
26442664
}
26452665

26462666
/** 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)