-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Associative, commutative, Java-friendly intersection erasure #11808
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
+1 −1 | shared/src/main/scala/scala/concurrent/stm/ccstm/ViewOps.scala |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import core.Flags._ | |
import core.Names.{DerivedName, Name, SimpleName, TypeName} | ||
import core.Symbols._ | ||
import core.TypeApplications.TypeParamInfo | ||
import core.TypeErasure.{erasure, isUnboundedGeneric} | ||
import core.TypeErasure.{erasedGlb, erasure, isUnboundedGeneric} | ||
import core.Types._ | ||
import core.classfile.ClassfileConstants | ||
import ast.Trees._ | ||
|
@@ -19,6 +19,7 @@ import TypeUtils._ | |
import java.lang.StringBuilder | ||
|
||
import scala.annotation.tailrec | ||
import scala.collection.mutable.ListBuffer | ||
|
||
/** Helper object to generate generic java signatures, as defined in | ||
* the Java Virtual Machine Specification, §4.3.4 | ||
|
@@ -65,18 +66,79 @@ object GenericSignatures { | |
|
||
def boxedSig(tp: Type): Unit = jsig(tp.widenDealias, primitiveOK = false) | ||
|
||
/** The signature of the upper-bound of a type parameter. | ||
* | ||
* @pre none of the bounds are themselves type parameters. | ||
* TODO: Remove this restriction so we can support things like: | ||
* | ||
* class Foo[A]: | ||
* def foo[S <: A & Object](...): ... | ||
* | ||
* Which should emit a signature `S <: A`. See the handling | ||
* of `AndType` in `jsig` which already supports `def foo(x: A & Object)`. | ||
*/ | ||
def boundsSig(bounds: List[Type]): Unit = { | ||
val (isTrait, isClass) = bounds partition (_.typeSymbol.is(Trait)) | ||
isClass match { | ||
case Nil => builder.append(':') // + boxedSig(ObjectTpe) | ||
case x :: _ => builder.append(':'); boxedSig(x) | ||
} | ||
isTrait.foreach { tp => | ||
val (repr :: _, others) = splitIntersection(bounds) | ||
builder.append(':') | ||
|
||
// According to the Java spec | ||
// (https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.4), | ||
// intersections erase to their first member and must start with a class. | ||
// So, if our intersection erases to a trait, in theory we should emit | ||
// just that trait in the generic signature even if the intersection type | ||
// is composed of multiple traits. But in practice Scala 2 has always | ||
// ignored this restriction as intersections of traits seem to be handled | ||
// correctly by javac, we do the same here since type soundness seems | ||
// more important than adhering to the spec. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wouldn't be a problem at all if our intersection erasure was only a trait when that trait dominates the intersection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is true, but I don't think that's a good enough reason to use that scheme as outlined in my other comment. |
||
if repr.classSymbol.is(Trait) then | ||
builder.append(':') | ||
boxedSig(repr) | ||
// If we wanted to be compliant with the spec, we would `return` here. | ||
else | ||
boxedSig(repr) | ||
others.filter(_.classSymbol.is(Trait)).foreach { tp => | ||
builder.append(':') | ||
boxedSig(tp) | ||
} | ||
} | ||
|
||
/** The parents of this intersection where type parameters | ||
* that cannot appear in the signature have been replaced | ||
* by their upper-bound. | ||
*/ | ||
def flattenedIntersection(tp: AndType)(using Context): List[Type] = | ||
val parents = ListBuffer[Type]() | ||
|
||
def collect(parent: Type, parents: ListBuffer[Type]): Unit = parent.widenDealias match | ||
case AndType(tp1, tp2) => | ||
collect(tp1, parents) | ||
collect(tp2, parents) | ||
case parent: TypeRef => | ||
if parent.symbol.isClass || isTypeParameterInSig(parent.symbol, sym0) then | ||
parents += parent | ||
else | ||
collect(parent.superType, parents) | ||
case parent => | ||
parents += parent | ||
|
||
collect(tp, parents) | ||
parents.toList | ||
end flattenedIntersection | ||
|
||
/** Split the `parents` of an intersection into two subsets: | ||
* those whose individual erasure matches the overall erasure | ||
* of the intersection and the others. | ||
*/ | ||
def splitIntersection(parents: List[Type])(using Context): (List[Type], List[Type]) = | ||
val erasedParents = parents.map(erasure) | ||
val erasedCls = erasedGlb(erasedParents).classSymbol | ||
parents.zip(erasedParents) | ||
.partitionMap((parent, erasedParent) => | ||
if erasedParent.classSymbol eq erasedCls then | ||
Left(parent) | ||
else | ||
Right(parent)) | ||
|
||
def paramSig(param: LambdaParam): Unit = { | ||
builder.append(sanitizeName(param.paramName)) | ||
boundsSig(hiBounds(param.paramInfo.bounds)) | ||
|
@@ -263,8 +325,20 @@ object GenericSignatures { | |
builder.append(')') | ||
methodResultSig(restpe) | ||
|
||
case AndType(tp1, tp2) => | ||
jsig(intersectionDominator(tp1 :: tp2 :: Nil), primitiveOK = primitiveOK) | ||
case tp: AndType => | ||
// Only intersections appearing as the upper-bound of a type parameter | ||
// can be preserved in generic signatures and those are already | ||
// handled by `boundsSig`, so here we fallback to picking a parent of | ||
// the intersection to determine its overall signature. We must pick a | ||
// parent whose erasure matches the erasure of the intersection | ||
// because javac relies on the generic signature to determine the | ||
// bytecode signature. Additionally, we prefer picking a type | ||
// parameter since that will likely lead to a more precise type. | ||
val parents = flattenedIntersection(tp) | ||
val (reprParents, _) = splitIntersection(parents) | ||
val repr = | ||
reprParents.find(_.typeSymbol.is(TypeParam)).getOrElse(reprParents.head) | ||
jsig(repr, primitiveOK = primitiveOK) | ||
|
||
case ci: ClassInfo => | ||
def polyParamSig(tparams: List[TypeParamInfo]): Unit = | ||
|
@@ -308,38 +382,6 @@ object GenericSignatures { | |
|
||
private class UnknownSig extends Exception | ||
|
||
/** The intersection dominator (SLS 3.7) of a list of types is computed as follows. | ||
* | ||
* - If the list contains one or more occurrences of scala.Array with | ||
* type parameters El1, El2, ... then the dominator is scala.Array with | ||
* type parameter of intersectionDominator(List(El1, El2, ...)). <--- @PP: not yet in spec. | ||
* - Otherwise, the list is reduced to a subsequence containing only the | ||
* types that are not supertypes of other listed types (the span.) | ||
* - If the span is empty, the dominator is Object. | ||
* - If the span contains a class Tc which is not a trait and which is | ||
* not Object, the dominator is Tc. <--- @PP: "which is not Object" not in spec. | ||
* - Otherwise, the dominator is the first element of the span. | ||
*/ | ||
private def intersectionDominator(parents: List[Type])(using Context): Type = | ||
if (parents.isEmpty) defn.ObjectType | ||
else { | ||
val psyms = parents map (_.typeSymbol) | ||
if (psyms contains defn.ArrayClass) | ||
// treat arrays specially | ||
defn.ArrayType.appliedTo(intersectionDominator(parents.filter(_.typeSymbol == defn.ArrayClass).map(t => t.argInfos.head))) | ||
else { | ||
// implement new spec for erasure of refined types. | ||
def isUnshadowed(psym: Symbol) = | ||
!(psyms exists (qsym => (psym ne qsym) && (qsym isSubClass psym))) | ||
val cs = parents.iterator.filter { p => // isUnshadowed is a bit expensive, so try classes first | ||
val psym = p.classSymbol | ||
psym.ensureCompleted() | ||
psym.isClass && !psym.is(Trait) && isUnshadowed(psym) | ||
} | ||
(if (cs.hasNext) cs else parents.iterator.filter(p => isUnshadowed(p.classSymbol))).next() | ||
} | ||
} | ||
|
||
/* Drop redundant types (ones which are implemented by some other parent) from the immediate parents. | ||
* This is important on Android because there is otherwise an interface explosion. | ||
*/ | ||
|
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 our last discussion, hadn't we come to the conclusion that we should never return an Interface as the erasure of an intersection type (unless an interface dominates everything else, of course)? IIRC the issue was that
Object & T1 & T2
would otherwise have a different erasure thanT1 & T2
, even thoughObject & T1 & T2
can simplify toT1 & T2
and though Java would erase toObject
.Uh oh!
There was an error while loading. Please reload this page.
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 went with this version originally but decided it wasn't worth it in the end:
Object & T1 & T2
has a different erasure thanT1 & T2
isn't great, but then so is the factObject & T1
has a different erasure thanT1
.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 see. IIUC, this is the critical aspect that allows this approach to work at all:
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.
It's helpful, but if those Java signatures weren't allowed, we could also emit a signature containing just the first trait as an approximation, we already have to use approximated signatures in various situations where our type system is more expressive than Java's.