-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #4884: Fix handling of bounds of type lambdas #4902
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
case prefix: ThisType => prefix.cls `eq` tycon.owner | ||
case NoPrefix => true | ||
case _ => false | ||
} |
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.
Can we further refine isTrivial
to cover the case when prefix
is static?
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.
I am not sure we want to do this. The purpose of isTrivial
is to fall back to a special case to avoid cyclic reference errors. It seems we don't need to include static this
types to achieve this. On the other hand, the isStatic
test could prompt cyclic references itself.
@odersky I got a new object Test {
trait A
trait B
trait TestConstructor1 { type F[X <: A]
trait FromSet[C[_ <: A with B]]
trait MSetLike[X <: A with B, This <: MSet[X] with MSetLike[X, This]] {
def to[C[X <: A with B] <: MSet[X] with MSetLike[X, C[X]]](fi: FromSet[C]): C[X] = ???
}
trait MSet[X <: A with B] extends MSetLike[X, MSet[X]]
object MSetFactory extends FromSet[MSet]
}
trait TestConstructor4[D] {
trait TestConstructor5[E] {
trait FromSet[C[_ <: D with E]]
trait MSetLike[X <: D with E, This <: MSet[X] with MSetLike[X, This]] {
def to[C[X <: D with E] <: MSet[X] with MSetLike[X, C[X]]](fi: FromSet[C]): C[X] = ???
}
trait MSet[X <: D with E] extends MSetLike[X, MSet[X]]
object MSetFactory extends FromSet[MSet]
}
}
type C = A & B
val v1: TestConstructor1 => Unit = { f =>
type P[a <: A] = f.F[a]
type P1[c <: C] = f.MSet[c]
(f.MSetFactory: f.FromSet[f.MSet]): Unit
(x: P1[C]) => x.to(f.MSetFactory)
}
def f3(f: TestConstructor4[A])(g: f.TestConstructor5[B]): Unit = {
type P1[c <: C] = g.MSet[c]
(g.MSetFactory: g.FromSet[g.MSet]): Unit
(x: P1[C]) => x.to(g.MSetFactory)
(x: P1[C]) => x.to[g.MSet](g.MSetFactory)
}
} Beware that Misc notes:
|
try self match { | ||
case self: TypeRef => | ||
val tsym = self.symbol | ||
if (tsym.isClass) tsym.typeParams | ||
else if (!tsym.exists) self.info.typeParams | ||
else if (!tsym.exists || !isTrivial(self.prefix, tsym)) self.info.typeParams |
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.
Does this prevent cyclic errors, or just reduce them enough? Otherwise, I agree this does the correct thing.
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.
There will always be cyclic errors. The challenge is to reduce them sufficiently that
- they occur only rarely
- they can be communicated so that they make sense to the user
- there is a way to work around them
and, probably most importantly
- they don't break existing code. What worked with the current compiler should continue working.
So it's really quite fuzzy. Ideally, we should have a spec that says in exactly what situations cyclic errors are OK. But we are currently quite far away from this point. I believe most mainstream languages that allow unlimited recursion face the same fundamental problem.
// wildcard identifiers `_` instead. | ||
res = res.withType(avoid(tparam.paramInfo, tpt1.tpe.typeParamSymbols)) | ||
res = res.withType(tparamBounds) |
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.
IIUC, avoiding C
's parameters was a hack, and substituting them with asSeenFrom was always the correct fix? Makes sense to me.
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.
Yes, I believe that's correct.
My actual goal with that test was to stress both asSeenFrom and a lazy denotation. It seems likely that the new code will trigger cyclic errors when |
@@ -1238,7 +1238,7 @@ class Typer extends Namer | |||
args = args.take(tparams.length) |
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.
The test failure seems genuinely to appear with this new commit (not with my changes) — everything works with the parent.
sbt:dotty> dotc -d out -Ycheck:all tests/pos/t1786-cycle.scala -Yforce-sbt-phases
[warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list
[info] Running (fork) dotty.tools.dotc.Main -classpath /Users/pgiarrusso/git/dotty/library/../out/bootstrap/dotty-library-bootstrapped/scala-0.10/dotty-library_0.10-0.10.0-bin-SNAPSHOT.jar -d out -Ycheck:all tests/pos/t1786-cycle.scala -Yforce-sbt-phases
checking tests/pos/t1786-cycle.scala after phase frontend
exception occurred while compiling tests/pos/t1786-cycle.scala
Exception in thread "main" dotty.tools.dotc.core.TypeError: bad parameter reference LongTraversableLike#Repr at sbt-deps
the parameter is type Repr in trait LongTraversableLike but the prefix LongTraversableLike
does not define any corresponding arguments.
at dotty.tools.dotc.core.Types$NamedType.argDenot(Types.scala:1834)
at dotty.tools.dotc.core.Types$NamedType.fromDesignator$1(Types.scala:1751)
at dotty.tools.dotc.core.Types$NamedType.computeDenot(Types.scala:1769)
at dotty.tools.dotc.core.Types$NamedType.denot(Types.scala:1725)
at dotty.tools.dotc.core.Types$NamedType.info(Types.scala:1714)
EDIT: cut stacktrace.
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 is fixed in the latest commit.
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.
Overall: I'd like to understand why the new typeParams shouldn't trigger a cyclic error, if that's the goal. Can we safely assume that non-trivial prefixes are already forced? Or do we consider such cyclic errors valid type errors (unless we can improve isTrivial
?).
(And sorry for the premature approval, wrong button).
8f91a82
to
0d13ba6
Compare
There are several undefined identifiers in the example as given. Can you check whether some errors still remain with the latest commit? |
I believe we have to consider them valid type errors, since the fallback strategy of computing parameters from symbols is not sound. But we are in a grey area here. For instance, one could introduce more laziness by delaying computation of the bounds of type parameters somehow. The question is always whether it's worth it. There are no hard truths to be had here. Essentially we are saying, given a evaluation strategy of the compiler, be as lazy as possible to prevent spurious cyclic references. The problem is there are many possible evaluation strategies, each with their different trade-offs. So we can never reach an optimum, we can only get sometimes gradual improvements (and even those might trade one source of cyclicity for another). |
- Type parameters of hk lambdas depend on prefix, can't be computed from symbol if prefix is non-trivial - Parameter bounds of such type parameters are already computed, so can be retrieved with paramInfo. The doc comment of paramInfoAsSeenFrom indicates this, but the implementation did something different. - Typer and Checking need to call paramInfoAsSeenFrom to get the right bounds in general. This also fixes scala#4884.
Using `tsym.info.typeParams` appears unnecessary in testing, and `info` seems always available so it's unclear it gives any savings.
The asSeenFrom is always wrt the type-constructor, not its prefix. This is because the parameter is treated as a member of the type constructor. It is strange that no test case caught this.
…ypeTree We need to take care that the asSeenFrom is from a * type, not a type constructor.
0d13ba6
to
e25e286
Compare
case info: LazyType => info.completerTypeParams(tsym) | ||
case info => info.typeParams | ||
case info: LazyType if tsym.exists && isTrivial(self.prefix, tsym) => info.completerTypeParams(tsym) | ||
case _ => self.info.typeParams |
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.
Why change this line from case info => info.typeParams
? If the info is already computed I would just reuse it
Edit: I realise it is not equivalent
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.
To clarify (for @allanrenucci and future readers): if tsym.exists && isTrivial(self.prefix, tsym)
, self.info.typeParams
and info.typeParams
should coincide and self.info.typeParams
is more clearly right, so I tried to simplify things.
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.
Actually I tried: info.typeParams
and self.info.typeParams
are not equivalent, the newly added test was failing. I think this is because in the former, info
may refer to the completer
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.
They’re only equivalent under those conditions, not in this pattern match branch.
I know that my commit didn’t change behavior in any scenario I tested with.
To play with this, you can go to the parent commit and replace info.typeParams
with self.info.typeParams
.
EDIT: that is, you can replace info.typeParams
by self.info.typeParams
in https://github.com/dotty-staging/dotty/blob/373902673f6a96018581ba2472d770686d718005/compiler/src/dotty/tools/dotc/core/TypeApplications.scala#L183, and it'll make no difference. At that point, since the conditional has two branches using self.info.typeParams
, this commit refactors the conditional to have one branch.
@odersky My example still doesn't work; sorry for the copy-paste mistakes, fixed them. Here are the errors I get:
|
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.
Overall the approach makes sense, and looks sound in the non-cyclic case. For the new error, dunno yet if it belongs here or to a separate issue, so I'll leave it to you @odersky.
Let's make a separate issue for the new error. |
OK, filed #4908. |
Type lambdas have their own type parameters which are correctly transformed during an as-seen-from. The problem was that these type parameters were not always retrieved when computing the type parameters of a higher-kinded type. This used to pick the type parameters of the symbol but that is wrong since it loses as-seen-from information.
It would be most straightforward to always compute the type parameters of the info instead, but this
runs afoul of cyclic errors. So we have a subtle danse now in
TypeApplications#typeParams
do decide what strategy to use when.Only last two commits are new wrt #4881.