-
Notifications
You must be signed in to change notification settings - Fork 1.1k
GADT pattern matching unsoundness. #3645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I believe (pure speculation based on intuition at this point) that the underlying issue is that type equality is used in negative positions in https://github.com/lampepfl/dotty/blob/7fa8a93694839b58b783128c2d8ae6098893a42f/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala#L220
sealed abstract class TypeComparison
final case object ProvablyEqual extends TypeComparison
final case object ProvablyUnequal extends TypeComparison
final case object Indeterminate extends TypeComparison In the example above it is impossible to prove that types |
Thanks a lot @alexknvl for reporting and the diagnosis. I just create a PR #3646 to handle this issue. |
The following snippet still compiles with the fix: object App {
def main(args: Array[String]): Unit = {
trait FooT {
type T
}
val Foo: FooT = new FooT {
type T = Int
}
type Foo = Foo.T
type Bar = Foo
sealed abstract class K[A]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
final case object K3 extends K[Bar]
def get(k: K[Int]): Unit = k match {
case K1 => ()
// case K2 => ()
// case K3 => ()
}
}
} but it is similarly unsound. |
Running the code with debug output shows that: candidates for K[Int] : [object K3, object K2, object K1]
[refine] unqualified child ousted: K3.type !< K[Int]
[refine] unqualified child ousted: K2.type !< K[Int]
K[Int] decomposes to [K1.type] which is incorrect. I believe that the offending lines are https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala#L628 and https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala#L634 (or rather corresponding |
@alexknvl If you write |
Here is an example that throws object App {
def main(args: Array[String]): Unit = {
trait FooT {
type T
def subst[F[_]](fa: F[T]): F[Int]
}
val Foo: FooT = new FooT {
type T = Int
def subst[F[_]](fa: F[T]): F[Int] = fa
}
type Foo = Foo.T
type Bar = Foo
sealed abstract class K[A]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
final case object K3 extends K[Bar]
val foo: K[Int] = Foo.subst[K](K2)
def get(k: K[Int]): Unit = k match {
case K1 => ()
// case K2 => ()
// case K3 => ()
}
get(foo)
}
} |
With the most recent fixes: object App {
def main(args: Array[String]): Unit = {
trait ModuleSig {
type Upper
trait FooSig {
type Type <: Upper
def subst[F[_]](fa: F[Int]): F[Type]
}
val Foo: FooSig
}
val Module: ModuleSig = new ModuleSig {
type Upper = Int
val Foo: FooSig = new FooSig {
type Type = Int
def subst[F[_]](fa: F[Int]): F[Type] = fa
}
}
type Upper = Module.Upper
type Foo = Module.Foo.Type
sealed abstract class K[F]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
val kv: K[Foo] = Module.Foo.subst[K](K1)
def test(k: K[Foo]): Unit = k match {
case K2 => ()
}
test(kv)
}
} fails at runtime with |
Found a new example that fails with the most recent commit in #3646 : object App {
def main(args: Array[String]): Unit = {
trait ModuleSig {
type U2
type U1
trait FooSig {
type Type = (U1 & U2)
def subst[F[_]](fa: F[Int]): F[Type]
}
val Foo: FooSig
}
val Module: ModuleSig = new ModuleSig {
type U1 = Int
type U2 = Int
val Foo: FooSig = new FooSig {
// type Type = Int
def subst[F[_]](fa: F[Int]): F[Type] = fa
}
}
type Foo = Module.Foo.Type
sealed abstract class K[F]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
val kv: K[Foo] = Module.Foo.subst[K](K1)
def test(k: K[Foo]): Unit = k match {
case K2 => ()
}
test(kv)
}
} |
Another one object App {
def main(args: Array[String]): Unit = {
trait ModuleSig {
type F[_]
type U
trait FooSig {
type Type = F[U]
def subst[F[_]](fa: F[Int]): F[Type]
}
val Foo: FooSig
}
val Module: ModuleSig = new ModuleSig {
type F[A] = Int
val Foo: FooSig = new FooSig {
// type Type = Int
def subst[F[_]](fa: F[Int]): F[Type] = fa
}
}
type Foo = Module.Foo.Type
sealed abstract class K[F]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
val kv: K[Foo] = Module.Foo.subst[K](K1)
def test(k: K[Foo]): Unit = k match {
case K2 => ()
}
test(kv)
}
} |
I think it's a safe bet that there's no solution to this in general without roles (https://ghc.haskell.org/trac/ghc/wiki/Roles). It's either that, or admit that we don't know if abstract type members are subtypes of other types. |
From a phone. AFAIU the problem can be roughly summarized as: given two
types A and B with some free type variables (+ constraints), a) is A <:< B
for all instantiations of type vars b) for some c) for none. I don't
completely understand the code, but would it be fair to say that if there
was a way to constructively solve (b), that would resolve this issue? The
code seems to be doing some sort of type normalization and then check <:<,
which might not work in general, since <:< instantiates all type vars as if
they are completely independent modulo constraints (?). Negation of it =
for a particular instantiation !(A <:< B), not for all instantiations.
I don't think that roles are directly relevant since here we are not really
dealing with newtypes, just abstract types.
…On Dec 11, 2017 12:55, "Edmund Noble" ***@***.***> wrote:
I think it's a safe bet that there's no solution to this in general
without roles (https://ghc.haskell.org/trac/ghc/wiki/Roles). It's either
that, or admit that we *don't know* if abstract type members are subtypes
of other types.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#3645 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAiH7io-BcP2r_uJITOZVC1Y2MND_-gTks5s_WyWgaJpZM4Q8g3Z>
.
|
Hm, on the other hand they are not really abstract (path.T for a stable
path) and in their behavior is very close to newtypes, so maybe it is
directly applicable :) I need to finish reading that paper...
…On Dec 11, 2017 13:25, "Alexander Konovalov" ***@***.***> wrote:
From a phone. AFAIU the problem can be roughly summarized as: given two
types A and B with some free type variables (+ constraints), a) is A <:< B
for all instantiations of type vars b) for some c) for none. I don't
completely understand the code, but would it be fair to say that if there
was a way to constructively solve (b), that would resolve this issue? The
code seems to be doing some sort of type normalization and then check <:<,
which might not work in general, since <:< instantiates all type vars as if
they are completely independent modulo constraints (?). Negation of it =
for a particular instantiation !(A <:< B), not for all instantiations.
I don't think that roles are directly relevant since here we are not
really dealing with newtypes, just abstract types.
On Dec 11, 2017 12:55, "Edmund Noble" ***@***.***> wrote:
> I think it's a safe bet that there's no solution to this in general
> without roles (https://ghc.haskell.org/trac/ghc/wiki/Roles). It's either
> that, or admit that we *don't know* if abstract type members are
> subtypes of other types.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <#3645 (comment)>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAiH7io-BcP2r_uJITOZVC1Y2MND_-gTks5s_WyWgaJpZM4Q8g3Z>
> .
>
|
@alexknvl you are right, the solution I proposed in #3646 is basically to say if there's some instantiation of type variables such that I'm yet to learn about roles, thanks for the pointer @edmundnoble . |
fix #3645: handle type alias in child instantiation
Reopening as it seems the issue is deeper than what #3646 fixed. |
👍 for the abstract types idea.
I only have a vague idea about the code in #3646, but your intuition about what quantification is needed makes sense. IIUC, the code tries to reduce @liufengyun any chance we can talk about this (maybe even tomorrow)? I'm sure I don't fully get the code but maybe we can help. trait ModuleSig {
type F[_]
type U
trait FooSig {
type Type = F[U]
def subst[F[_]](fa: F[Int]): F[Type]
}
val Foo: FooSig
}
class ModuleUser(Module: ModuleSig) {
def foo(): Unit = {
type Foo = Module.Foo.Type
sealed abstract class K[F]
final case object K1 extends K[Int]
final case object K2 extends K[Foo]
val kv: K[Foo] = Module.Foo.subst[K](K1)
def test(k: K[Foo]): Unit = k match {
case K2 => ()
}
test(kv)
}
}
object App {
val Module: ModuleSig = new ModuleSig {
type F[A] = Int
val Foo: FooSig = new FooSig {
// type Type = Int
def subst[F[_]](fa: F[Int]): F[Type] = fa
}
}
def main(args: Array[String]): Unit =
new ModuleUser(Module).foo()
} |
I'm looking at roles, but I'm still wondering if they're indeed applicable to our Scala scenario — I haven't decided but I still think not. Maybe I'm missing something? Looking at "Generative Type Abstraction and Type-level Computation":
data K a where
KAge :: K Age
KInt :: K Int
get :: K Age→ String
get KAge = "Age" and claim that
But claiming Overall, the goal is to take some hidden type equalities, and make them visible at role "type" (Sec. 3). But when typechecking They add in Sec. 3.2 that:
but since Scala has abstract types, we can soundly write the coercions we want ourselves (as you did), and they're sound by themselves, it's hard to blame the coercion lifting, and much easier to blame the match.
|
Took a look, talked with @liufengyun, tried the examples, and #3646 seems to fix all the given ones. @liufengyun also explained me why using In particular, I learned the answer to this question:
In Dotty Plan: check the examples are covered by testcases and close the issue. |
Since even the last example is in https://github.com/lampepfl/dotty/pull/3646/files#diff-4511d6b273cb5ec2facf7dbd0ef33330, closing. |
Sorry this was for another issue, doh! |
Produces no warnings but results in a runtime
MatchError
failure.Somewhat relevant paper (it describes a similar problem in Haskell that was solved by the introduction of
role
s, but I am not sure how applicable it is in the context of Scala).The text was updated successfully, but these errors were encountered: