-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Generalize Approximating Type Maps #2908
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
34dfff1
to
2c46850
Compare
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.
Very nice refactoring 👍
case tp: ThisType => | ||
toPrefix(pre, cls, tp.cls) | ||
case _: BoundType | NoPrefix => | ||
tp |
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.
tp == NoType
will go all the way up to TypeMap.mapOver
and test for all the cases. Maybe handle it here or even earlier?
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 believe tp == NoType
will only happen in exceptional cases. So we don't want to slow down the general logic by testing for it early.
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 during the debugging, it's quite common, triggered from the Namer: https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Namer.scala#L1011
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.
That's probably still a lot less than the other cases. To be sure either way we'd have to measure (i.e. add counters).
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 agree, it's better to handle that special case in Namer
instead of AsSeenFromMap
.
|
||
override def toText(printer: Printer): Text = | ||
lo.toText(printer) ~ ".." ~ hi.toText(printer) | ||
} |
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.
Maybe add a note to Range
-- its usage is limited to ApproximatingTypeMap
, which is very special compared to other types.
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, good idea.
def distributeArgs(args: List[Type], tparams: List[ParamInfo]): Boolean = args match { | ||
case Range(lo, hi) :: args1 => | ||
val v = tparams.head.paramVariance | ||
if (v == 0) 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.
Missing "return" before "false"? Otherwise, it seems distributeArgs
always return true
.
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.
Good catch. Wrong logic here.
else | ||
curOwner | ||
effectiveOwner.thisType.select(name, defDenot) | ||
} |
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 still have no idea why this change becomes necessary despite the comments. Is it due to the change of variance for NamedTypes in TypeMaps?
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.
Approximating type maps now work with bounds instead of NoTypes. This gives more precision (in particular for type applications), and also makes it possible to combine approximation with other mappings. As a side-effect we provide the hooks for preventing constructing illegal types C#A where C is non-singleton and A is abstract by setting variance to 0 for the prefix of an abstract type selection. There's one test case that fails: One of the types in dependent-exractors.scala does not check out anymore. This has likely to do with the loss of precision incurred by the maps. Exact cause remains to be tracked down.
A Range, if it survives, will always lead to a situation where the upper bound appears in a covariant position in the result, and the lower bound appears in a contravariant position. Hence, when we apply a type map to the argument in a range we should take this into account. Fixes a previous failure in t2435.scala and dependent-extractors.scala.
Supersedes old scheme of dealing with unstable prefixes in non-variant positions.
It's no longer needed.
- Use range instead of Range in AsSeenFromMap#apply. We needed Range before because we did an incorrect variance computation for NamedTypes. - Refine derivedSelect
a5a59b5
to
89180b5
Compare
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 really like the simplification!
} | ||
|
||
override protected def derivedClassInfo(tp: ClassInfo, pre: Type): Type = { | ||
assert(!pre.isInstanceOf[Range]) |
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 could use isRange
defined above. Also, why is this condition always true?
if (tp1.exists && tp2.exists) tp.derivedAndOrType(tp1, tp2) | ||
else if (tp.isAnd) approx(hi = tp1 & tp2) // if one of tp1d, tp2d exists, it is the result of tp1d & tp2d | ||
else approx(lo = tp1 & tp2) | ||
if (tp1.isInstanceOf[Range] || tp2.isInstanceOf[Range]) |
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 could use isRange
defined above.
try lo =:= hi | ||
catch { case ex: Throwable => false } | ||
|
||
private def homogenizeArg(tp: Type) = tp match { |
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 wouldn't use the word homogenize
here since it's already used in the printer for the specific usecase of comparing trees before and after pickling.
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.
In fact, the name was correct, but the condition to do this only under homogenizedView
was lacking.
@@ -68,6 +68,15 @@ class PlainPrinter(_ctx: Context) extends Printer { | |||
} | |||
else tp | |||
|
|||
private def sameBound(lo: Type, hi: Type): Boolean = | |||
try lo =:= hi |
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.
Shouldn't this be frozen_=:=
?
@@ -68,6 +68,15 @@ class PlainPrinter(_ctx: Context) extends Printer { | |||
} | |||
else tp | |||
|
|||
private def sameBound(lo: Type, hi: Type): Boolean = | |||
try lo =:= hi | |||
catch { case ex: Throwable => 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.
I think this is a bit sloppy :). I would like NonFatal(ex)
more, or even better: define a base trait that all non-fatal exceptions we throw in the typechecker must extend. (Or even even better: make the high-level APIs of the typer deal with exceptions themselves, clearly there is no value in propagating the exceptions up more if we're just ignoring them).
range(derivedRefinedType(tp, parent, lo), derivedRefinedType(tp, parent, hi)) | ||
tp.refinedInfo match { | ||
case rinfo: TypeBounds => | ||
val v = if (rinfo.isAlias) rinfo.variance * variance else variance |
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 don't understand what's going on here, could you add some comments? I've also checked and we have zero test cases where v does not end up being 0 here, so it's hard to judge the validity of the code.
case rinfo: TypeBounds => | ||
val v = if (rinfo.isAlias) rinfo.variance * variance else variance | ||
if (v > 0) tp.derivedRefinedType(parent, tp.refinedName, rangeToBounds(info)) | ||
else if (v < 0) propagate(infoHi, infoLo) |
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 find this especially suspicious, this seems to violate the invariant that Range#lo
is a subtype of Range#hi
, am I missing something?
if (args.exists(isRange)) { | ||
if (variance > 0) tp.derivedAppliedType(tycon, args.map(rangeToBounds)) | ||
else { | ||
val loBuf, hiBuf = new mutable.ListBuffer[Type] |
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 think this case is also complicated enough to warrant a comment :).
case _ => tp | ||
} | ||
|
||
protected def atVariance[T](v: Int)(op: => T): T = { |
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 think this is worth putting in TypeMap
itself, considering how common the pattern val saved = variance; variance = ...
is. Also maybe worth marking inline
?
|
||
protected def range(lo: Type, hi: Type) = |
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.
Calling this method range
makes it a bit too easy to confuse it with Range
. Perhaps call it between
?
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 current situation is analogous to range
/ Range
for collection ranges. I believe that's a good thing.
The logic before was overcomplicated since I did not take into account that Range refinedInfos could only happen at variance 0. On the othet hand, it did not take into account all the subtleties of alias types with variances.
7818ed3
to
4160921
Compare
I'll roll the comment fixes into #2945. Can you review that one next? 😄 |
Sure! |
test performance please |
performance test scheduled: 1 jobs in total. |
Performance finished successfully: Visit https://liufengyun.github.io/bench/2908 to see the changes. |
Approximating type maps now work with bounds instead of NoTypes. This
gives more precision (in particular for type applications), and also
makes it possible to combine approximation with other mappings.
The biggest win here is that we can now express AsSeenFrom soundly in terms of ApproximatingTypeMaps. We can therefore remove the UnsafeNonVariant annotations and
retroactive skolemizations which felt kludgey.