-
Notifications
You must be signed in to change notification settings - Fork 1.1k
IndexOutOfBoundsException when attempting to reduce ill-kinded types #2887
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
Here is another example, producing a stack overflow again (like for #2771). It uses the untyped SKI fixpoint combinator and absurd bounds to create a non-terminating term. trait A { type S[X[_] <: [_] => Any, Y[_]] <: [_] => Any; type I[_] }
trait B { type S[X[_],Y[_]]; type I[_] <: [_] => Any }
trait C { type M <: B }
trait D { type M >: A }
object Test {
def test(x: C with D): Unit = {
def foo(a: A, b: B)(z: a.S[b.I,a.I][b.S[a.I,a.I]]) = z
def bar(a: A, y: x.M) = foo(a,y)
def baz(a: A) = bar(a, a)
baz(new A { type S[X[_] <: [_] => Any, Y[_]] = [Z] => X[Z][Y[Z]]; type I[X] = X })(1)
}
} Again, the error disappears when the call to Note the use of curried type operators. One can also write an uncurried version, resulting in yet another error: trait A { type S[X[_,_], Y[_],_]; type I[_] }
trait B { type S[X[_],Y[_]]; type I[_,_] }
trait C { type M <: B }
trait D { type M >: A }
object Test {
def test(x: C with D): Unit = {
def foo(a: A, b: B)(z: a.S[b.I,a.I,b.S[a.I,a.I]]) = z
def bar(a: A, y: x.M) = foo(a,y)
def baz(a: A) = bar(a, a)
baz(new A { type S[X[_,_], Y[_], Z] = X[Z,Y[Z]]; type I[X] = X })(1)
}
} produces the error
Interestingly, the error does not disappear in this case when call to Note that none of these examples uses recursion (neither on the term level nor on the type level). All the trouble is caused by bad bounds. |
This looks pretty troubling to me. What is a principled way to rule out these problems? I mean, without throwing out higher-kinded types or intersections altogether? It would be great if someone went to the bottom of this and came up with a proposal what we should do. I personally am more and more disgusted by the hidden complexities caused by higher-kinded types. If things stay as they are I see no alternative to bringing back the higher-kinded language import and declaring higher-kinded types officially unsound. |
You might ask: Why does scalac refuse the program? It's because it does not have true intersections. In
the type of |
A few comments up front:
Given that this is fundamentally a compile-time issue, it seems to me that the best one can do is provide better error messages. What else is there? My pragmatic solution would be: don't try to exclude this sort of thing statically but treat it as a sort of "run-time error" for type-level computation: report a compile error when the arities of type operators and their argument lists don't match; impose a limit on the time/number of calls spent reducing types to avoid non-termination. This is not a perfect solution, but I think it will catch most real-world issues. There might be a "cleaner" but much more complex solution which involves tracking uses of subsumption in type expressions, and not allowing type reductions unless those can be statically guaranteed to be consistent. E.g. in the SKI example, the return-type of |
I agree the problem is a combination of higher-kinded types and bad bounds. From the work on DOT it seems unlikely that bad bounds can be detected a priori. And it's not clear it can lead to type soundness problems or "just" to misbehaving compilers. This means we are in the unenviable situation that we cannot guarantee the preconditions the bulk of our type checking operations anymore - they might all be wrong due to incompatible kinds that we cannot detect. Sure we could make our type computations more robust by dealing with all sort of illegal situations, imposing arbitrary limits on stack depth and so on. But that has an unfortunate tendency of hiding true error conditions where we want to crash because something is wrong. For a compiler writer this is a nightmare scenario. |
OK that is a very good point. But then I'm wondering, isn't that already true to some extent for ill-typed terms? The following example type checks, but if we ever tried to evaluate the body of trait A { type L >: Int => Int }
trait B { type L <: Int => Int => Int }
object Test {
def test(x: A with B): Unit = {
def badCast(f: Int => Int): x.L = f
val f : Int => Int = (y: Int) => y
val fBad : Int => Int => Int = badCast(f)
fBad(1)(1)
}
} |
You can inline while preserving typecheckability, you just need to introduce casts: f.asInstanceOf[Int => Int => Int](1)(1) |
Maybe the equivalent of runtime type-casting is compile-time kind-casting :). |
Sure, but then the casts prevent your from simplifying the expression. (Side note: I wonder how Scala-JS deals with this sort of thing. AFAIK it performs quite aggressive inlining and simplifications. Maybe @sjrd can comment on this).
Or compile-time type-casting on paths, see my earlier comment. And as I mention there, that doesn't quite solve the problem. Just as type-casts stop you from simplifying term-level expressions, they stop you from evaluating type expressions unless you can prove that it's safe to remove them. It's that latter part that is difficult. |
👍 💯 to this.
Wait—why can one not detect that However, my contention only allows (at best) patching known examples, so I agree this is unsatisfying. *As long as you don't actually substitute |
Regarding coercions in ASTs: I don't think stuck terms/types would be a problem—as long as long as you have the theory needed to simplify safely enough expressions. Since producing this theory is nontrivial research, I agree with @sstucki that coercions can't be used yet. Also, using coercions here seems out of "balance". In contrast, GHC uses coercions to handle GADTs, even though they sometimes interfere with optimizations—while Scalac (and I assume Dotty) don't. Coercions might be needed as input for some type-preserving transformations; but even otherwise, having to produce evidence that GADT coercions are safe makes GHC more robust—in other words, that's technology to increase robustness. IMHO, implementing coercions to handle GADTs would matter more to user experience than using them for higher-kinded types. But even there, research (and volunteers for it) is needed :-) |
I believe the counter example would involve a type |
Exactly. Here's an example: trait A { type L; type U; type T }
trait B extends A { type T >: L }
trait C extends A { type T <: U }
trait Cast[X <: B, Y <: C] {
def cast(x: X & Y)(z: x.L):x.U = (z: x.T)
}
trait Lower extends B { type L = Int }
trait GoodUpper extends C { type U = Int }
trait BadUpper extends C { type U = Int => Int }
object GoodCast extends Cast[Lower, GoodUpper]
object BadCast extends Cast[Lower, BadUpper] The scalac rejects the program with the error
|
Scala.js, like Scala/JVM, introduces the necessary casts during I'm not sure whether this is actually relevant to the present discussion, though. |
That makes sense, provided that If it's not the case, then there are some other subtleties here, like
I think it is. |
Yes, after erasure (or, at the very least, once at the IR/bytecode level), subtyping relationships that hold at compile/link time are guarantee to still be valid at run-time. It is not the case before erasure. |
I'm not trying to prevent ill-kinded types. They won't crash Dotty. Code using them will. But I might have realized why I misunderstand what you meant by "detecting a priori". Does Dotty expect that reducing open types is kind-sound? Sure, that doesn't work, so I agree one should just detect when reducing types produces actual dynamic kind errors. But yeah, I see why that's probably your "nightmare scenario". To distinguish Dotty bugs from exploits of actually inconsistent boundaries, I think one can tune where errors are detected. Normally, you just need to detect violations of a sort of "canonical forms lemma" for type reductions—for instance, having a binary type constructor when a unary one is expected, like more or less here. This is where HK-types might be relevant: without HK-types all kinds are subkinds of When debugging the compiler or type errors, you probably want to detect (and reject) actual inconsistency—not like But I'll confess I'm not sure yet where the original example should be rejected: at the earliest, |
I think this should be re-opened because the problem persists (checked with dotty 3.0.0-M2). There's an error in one of the regression tests committed by @nicolasstucki that makes it fail, but it's just a syntax error (the syntax of type lambdas has changed form Here's the fixed version of trait A { type S[X[_] <: [_] =>> Any, Y[_]] <: [_] =>> Any; type I[_] } // error // error
trait B { type S[X[_],Y[_]]; type I[_] <: [_] =>> Any } // error
trait C { type M <: B }
trait D { type M >: A }
object Test {
def test(x: C with D): Unit = {
def foo(a: A, b: B)(z: a.S[b.I,a.I][b.S[a.I,a.I]]) = z // error
def bar(a: A, y: x.M) = foo(a,y)
def baz(a: A) = bar(a, a)
baz(new A { type S[X[_] <: [_] =>> Any, Y[_]] = [Z] =>> X[Z][Y[Z]]; type I[X] = X })(1) // error // error
}
} |
…well, I wish I remembered/could recall why I started tracking this issue/what made me Stert tracking it, I know it was (and probably still is? Is say damn my memory, but already seems pretty damned.) important, but I apparently didn't and/or can't, but I'm glad to see it fixed. Thanks @odersky ! |
The following snippet is a variant of that causing #2771:
The underlying issue is likely the same, though the error message is different:
The error disappears when the last line (the call to
baz
) is commented out. See #2771 for a more in-depth discussion.The text was updated successfully, but these errors were encountered: