-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fixed #264 - failure to typecheck GADTs #271
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
Conversation
Who would like to review this? |
@Blaisorblade's investigations into the limitations of scalac's GADT support might be worth a look while you're thinking about this. In the comments, I minimized the problem to: package bugreport
object BugReport {
trait Type {
type DT <: Type
}
trait BaseInt extends Type {
type DT = BaseInt
}
trait Exp[TP <: Type]
class Num extends Exp[BaseInt]
def derive[T <: Type](term: Exp[T]): Any = {
val res1 = term match { case _: Num => (??? : Exp[T#DT]) }
res1: Exp[T#DT] // fails, the prefix of the type projection in the case body underwent unwanted GADT refinement
type X = T#DT
val res2 = term match { case _: Num => (??? : Exp[X]) }
res2: Exp[T#DT] // works
}
} That fails in scalac with:
But is allowed in dotc as of this PR. However, the main example in that Gist fails in both compilers (albeit with different error messages)
|
In the process of playing around with that test case, I discovered a dotc crasher. trait Type {
type DT
}
trait Exp[type T <: Type] {
def derive: Exp[T#DT]
} Fails with:
|
/** The current bounds in force for type parameters appearing in a GADT */ | ||
private var _gadt: GADTMap = _ | ||
protected def gadt_=(gadt: GADTMap) = _gadt = gadt | ||
def gadt: GADTMap = _gadt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This encaspulates the ability to assign a new map, but leaves the door open for anyone to mutate GADTMap#bounds
.
I'd prefer to see something like:
class GADTMap {
private var bounds = new SimpleMap
def narrowBounds(sym: Symbol, bounds: TypeBounds): Unit = {
bounds = bounds.updated(symbol, bounds)
}
def apply(sym: Symbol): TypeBounds = bounds(sym)
}
regarding @Blaisorblade's gist, I think the errors reported by dotc are intentional. We are stricter than scalac when checking legal projections. scalac converts them to existentials, which dotc does not have. |
The previous scheme derived the right bounds, but then failed to use them because a TypeRef already has a set info (its bounds). Changing the bounds in the symbol by a side effect does not affect that. This is good! But it showed that the previous scheme was too fragile because it used a sneaky side effect when updating the symbol info which failed to propgate into the cached info in TypeRef. We now keep GADT computed bounds separate form the symbol info in a map `gadt` in the current context.
a0eee45
to
009a1e6
Compare
Hi, thanks for the mention!
Is the new criterion explained somewhere? That sentence makes vaguely sense to me, since ML modules are even more restrictive — though I don't fully remember why. Is that projection volatile without desugaring?
As in, T#DT used to become t.DT forSome { val t: T } which is now forbidden? def derive(t: Type)(term: Exp[t.type]): Exp[t.DT] =
term match {
case Num(n) => Num(n)
//val a = Num(n): Exp[v.DT]
//a
}
object BaseInt extends BaseType[Int] { // this was a sealed trait with no children, but no more!
type DT = BaseInt
} so we'd be able to wouldn't limit expressiveness much. As you probably remember, I'd like to make def derive[[t: Type]](term: Exp[t.type]): Exp[t.DT] = so we wouldn't create boilerplate, but that's a separate issue. (BTW, in the Agda version one would have t inferred by unification rather than implicit search, with a different kind of implicit — I'm not sure if conflating the two would actually work). |
The explanation of the restriction is in Typer.scala:
It's one of the spots where I am not sure yet what to do because the theory is not worked out. Motivation: We definitely need to support A # B for classes A, B because that's what Java's inner classes are. We also need to support T # X for a single abstract type X because that's the encoding of an instantiated higher-kinded type.E.g. Rep[String] for abstract type Rep = Lambda { type Apply; type hk$Arg$0 } The idea is to regard a type T # X as a skolem, similar to a path dependent type p.T. So the only info we have about the type are the bounds of X. But unlike an existential it is a first-class type, which does not need to be packed/unpacked. It's easy to show that this is unsound if T contains abstract types besides X: Just mix two different implementations of these abstract types. But if X is the only abstract type member, nobody could shoot holes in this so far. Nevertheless it would be important to work out the theory of this, and if necessary revise. So the current status is just a snapshot. |
The crasher exercises named type parameters, which are not really supported yet. I'll move a test into pending so that we remember to come back to it. |
Fixed #264 - failure to typecheck GADTs
Backport "completions: do not complete package" to 3.3 LTS
The previous scheme derived the right bounds, but then failed to use them
because a TypeRef already has a set info (its bounds). Changing the bounds in
the symbol by a side effect does not affect that. This is good! But it showed
that the previous scheme was too fragile because it used a sneaky side effect
when updating the symbol info which failed to propgate into the cached
info in TypeRef.
We now keep GADT computed bounds separate form the symbol info
in a map
gadt
in the current context.Closes #268. (Branch got wrong ticket number due to a typo).