Skip to content

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

Closed
wants to merge 13 commits into from

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jul 24, 2017

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.

@odersky odersky changed the title Generalize Approximating Type Maps [WIP] Generalize Approximating Type Maps Jul 25, 2017
@odersky odersky force-pushed the change-approx-typemap branch 2 times, most recently from 34dfff1 to 2c46850 Compare July 31, 2017 15:58
@odersky odersky requested review from smarter and liufengyun August 2, 2017 15:55
@odersky odersky changed the title [WIP] Generalize Approximating Type Maps Generalize Approximating Type Maps Aug 2, 2017
Copy link
Contributor

@liufengyun liufengyun left a 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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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).

Copy link
Contributor

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)
}
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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)
}
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure.

odersky added 8 commits August 9, 2017 10:23
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
@odersky odersky force-pushed the change-approx-typemap branch from a5a59b5 to 89180b5 Compare August 9, 2017 08:43
Copy link
Member

@smarter smarter left a 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])
Copy link
Member

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])
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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 }
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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]
Copy link
Member

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 = {
Copy link
Member

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) =
Copy link
Member

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 ?

Copy link
Contributor Author

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.
@odersky odersky force-pushed the change-approx-typemap branch from 7818ed3 to 4160921 Compare August 13, 2017 17:24
@odersky
Copy link
Contributor Author

odersky commented Aug 13, 2017

I'll roll the comment fixes into #2945. Can you review that one next? 😄

@smarter
Copy link
Member

smarter commented Aug 13, 2017

Sure!

@liufengyun
Copy link
Contributor

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 jobs in total.

@dottybot
Copy link
Member

Performance finished successfully:

Visit https://liufengyun.github.io/bench/2908 to see the changes.

@smarter
Copy link
Member

smarter commented Aug 22, 2017

Merged as part of #2970 (and also later as part of #2945)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants