unsound and non-transitive subtyping behavior with qualified types
Opened this issue · 1 comments
Given an interface I
and a nested interface I.J
, it's possible to define a type A.B.C
which satisfies I.J
as both <A of I>.<C of J>
and <A.B of I>.<C of J>
.
This is possible when
A
,A.B
, andA.B.C
extendASup
,ASup.BSup
, andASup.BSup.CSup
respectivelyA
andA.B
both satisfyI
ASup.BSup
does not satisfyI
A.B.C
andASup.BSup.CSup
both satisfyI.J
If I
has type parameters, the two supertypes I.J
may differ.
Two problems occur in 1.3.4-SNAPSHOT:
- The typechecker doesn't require
A.B.C
to refineI.J
's members to properly satisfy the requirements of<A.B of I>.<C of J>
. This can result in unsound behavior. - Transitive subtyping is violated;
A.B.C
can be widened toASup.BSup.CSup
which can be widened the (generic) type that correspends to<A of I>.<C of J>
, butA.B.C
cannot be widened to<A of I>.<C of J>
directly.
Sample code for 1.3.4-SNAPSHOT (the behavior in 1.3.3 is slightly different):
interface I<out T> {
shared interface J {
shared formal T getT;
}
}
class ASup<T>(T t) satisfies I<T> {
shared default class BSup() {
shared default class CSup() satisfies J { // J's outer is ASup<T>
shared actual T getT => t;
}
}
}
class A<T>(T t) extends ASup<T>(t) {
shared class B<U>() extends super.BSup() satisfies I<U> {
shared class C() extends super.CSup() satisfies J {} // J's outer is B<U>
}
}
shared void run() {
A<String>.B<Integer>.C abc = A<String>("x").B<Integer>().C();
ASup<String>.BSup.CSup abcSup= abc;
noop((abc of I<Integer>.J).getT of Integer); // Assigning a String to an Integer!
noop((abcSup of I<String>.J).getT of String); // ok
noop((abc of ASup<String>.BSup.CSup) of I<String>.J); // ok
noop( abc of I<String>.J); // error (subtyping is not transitive)
// error: specified type does not cover the cases of the operand expression: 'I<String>.J' does not cover 'A<String>.B<Integer>.C'
}
I'm not sure the ability to "skip" outer types when satisfying a nested type (e.g. allowing CSup satisfies J
despite BSup
being in the way) is worth the typesystem and backend complexity, although maybe I'm missing some obviously useful example.
Without this "skipping", inner classes would only ever have a single outer
capture that would be applicable for all supertypes that are nested types, and a conceptual Outer
type would exist (as envisioned by @Zambonifofex in https://gitter.im/ceylon/user?at=5a673dd298927d57452691ba), which would simply be the qualifying type.
(Edit: actually, I suppose a change to the subtyping rules would also be required to support @Zambonifofex's Outer
type)