|
| 1 | +package dotty.tools |
| 2 | +package dotc |
| 3 | +package core |
| 4 | +package unpickleScala2 |
| 5 | + |
| 6 | +import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Phases._ |
| 7 | +import Decorators._ |
| 8 | +import backend.sjs.JSDefinitions |
| 9 | +import scala.collection.mutable.ListBuffer |
| 10 | + |
| 11 | +/** Erasure logic specific to Scala 2 symbols. */ |
| 12 | +object Scala2Erasure: |
| 13 | + /** Is this a supported Scala 2 refinement or parent of such a type? |
| 14 | + * |
| 15 | + * We do not allow types that look like: |
| 16 | + * ((A with B) @foo) with C |
| 17 | + * or: |
| 18 | + * (A { type X <: ... })#X with C` |
| 19 | + * |
| 20 | + * as it would make our implementation of Scala 2 intersection erasure |
| 21 | + * significantly more complicated. The problem is that each textual |
| 22 | + * appearance of an intersection or refinement in a parent corresponds to a |
| 23 | + * fresh instance of RefinedType (because Scala 2 does not hash-cons these |
| 24 | + * types) with a fresh synthetic class symbol, thus affecting the result of |
| 25 | + * `isNonBottomSubClass`. To complicate the matter, the Scala 2 UnCurry phase |
| 26 | + * will also recursively dealias parent types, thus creating distinct class |
| 27 | + * symbols even in situations where the same type alias is used to refer to a |
| 28 | + * given refinement. Note that types like `(A with B) with C` do not run into |
| 29 | + * these issues because they get flattened into a single RefinedType with |
| 30 | + * three parents, cf `flattenedParents`. |
| 31 | + * |
| 32 | + * See sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala for examples. |
| 33 | + * |
| 34 | + * @throws TypeError if this type is unsupported. |
| 35 | + */ |
| 36 | + def supportedType(tp: Type)(using Context): Unit = tp match |
| 37 | + case AndType(tp1, tp2) => |
| 38 | + supportedType(tp1) |
| 39 | + supportedType(tp2) |
| 40 | + case RefinedType(parent, _, _) => |
| 41 | + supportedType(parent) |
| 42 | + case AnnotatedType(parent, _) if parent.dealias.isInstanceOf[Scala2RefinedType] => |
| 43 | + throw new TypeError(i"Unsupported Scala 2 type: Component $parent of intersection is annotated.") |
| 44 | + case tp @ TypeRef(prefix, _) if !tp.symbol.exists && prefix.dealias.isInstanceOf[Scala2RefinedType] => |
| 45 | + throw new TypeError(i"Unsupported Scala 2 type: Prefix $prefix of intersection component is an intersection or refinement.") |
| 46 | + case _ => |
| 47 | + |
| 48 | + /** A type that would be represented as a RefinedType in Scala 2. |
| 49 | + * |
| 50 | + * The `RefinedType` of Scala 2 contains both a list of parents |
| 51 | + * and a list of refinements, intersections are represented as a RefinedType |
| 52 | + * with no refinements. |
| 53 | + */ |
| 54 | + type Scala2RefinedType = RefinedType | AndType |
| 55 | + |
| 56 | + /** A TypeRef that is known to represent a member of a structural type. */ |
| 57 | + type StructuralRef = TypeRef |
| 58 | + |
| 59 | + /** The equivalent of a Scala 2 type symbol. |
| 60 | + * |
| 61 | + * In some situations, nsc will create a symbol for a type where we wouldn't: |
| 62 | + * |
| 63 | + * - `A with B with C { ... }` is represented with a RefinedType whose |
| 64 | + * symbol is a fresh class symbol whose parents are `A`, `B`, `C`. |
| 65 | + * - Structural members also get their own symbols. |
| 66 | + * |
| 67 | + * To emulate this, we simply use the type itself a stand-in for its symbol. |
| 68 | + * |
| 69 | + * See also `sameSymbol` which determines if two pseudo-symbols are really the same. |
| 70 | + */ |
| 71 | + type PseudoSymbol = Symbol | StructuralRef | Scala2RefinedType |
| 72 | + |
| 73 | + /** The pseudo symbol of `tp`, see `PseudoSymbol`. |
| 74 | + * |
| 75 | + * The pseudo-symbol representation of a given type is chosen such that |
| 76 | + * `isNonBottomSubClass` behaves like it would in Scala 2, in particular |
| 77 | + * this lets us strip all aliases. |
| 78 | + */ |
| 79 | + def pseudoSymbol(tp: Type)(using Context): PseudoSymbol = tp.widenDealias match |
| 80 | + case tpw: Scala2RefinedType => |
| 81 | + supportedType(tpw) |
| 82 | + tpw |
| 83 | + case tpw: TypeRef => |
| 84 | + val sym = tpw.symbol |
| 85 | + if !sym.exists then |
| 86 | + // Since we don't have symbols for structural type members we use the |
| 87 | + // type itself and rely on `sameSymbol` to determine whether two |
| 88 | + // such types would be represented with the same Scala 2 symbol. |
| 89 | + tpw |
| 90 | + else |
| 91 | + sym |
| 92 | + case tpw: TypeProxy => |
| 93 | + pseudoSymbol(tpw.underlying) |
| 94 | + case tpw: JavaArrayType => |
| 95 | + defn.ArrayClass |
| 96 | + case tpw: OrType => |
| 97 | + pseudoSymbol(TypeErasure.scala2Erasure(tpw)) |
| 98 | + case tpw: ErrorType => |
| 99 | + defn.ObjectClass |
| 100 | + case tpw => |
| 101 | + throw new Error(s"Internal error: unhandled class ${tpw.getClass} for type $tpw in pseudoSymbol($tp)") |
| 102 | + |
| 103 | + extension (psym: PseudoSymbol)(using Context) |
| 104 | + /** Would these two pseudo-symbols be represented with the same symbol in Scala 2? */ |
| 105 | + def sameSymbol(other: PseudoSymbol): Boolean = |
| 106 | + // Pattern match on (psym1, psym2) desugared by hand to avoid allocating a tuple |
| 107 | + if psym.isInstanceOf[StructuralRef] && other.isInstanceOf[StructuralRef] then |
| 108 | + val tp1 = psym.asInstanceOf[StructuralRef] |
| 109 | + val tp2 = other.asInstanceOf[StructuralRef] |
| 110 | + // Two structural members will have the same Scala 2 symbol if they |
| 111 | + // point to the same member. We can't just call `=:=` since different |
| 112 | + // prefixes will still have the same symbol. |
| 113 | + (tp1.name eq tp2.name) && pseudoSymbol(tp1.prefix).sameSymbol(pseudoSymbol(tp2.prefix)) |
| 114 | + else |
| 115 | + // We intentionally use referential equality here even though we may end |
| 116 | + // up comparing two equivalent intersection types, because Scala 2 will |
| 117 | + // create fresh symbols for each appearance of an intersection type in |
| 118 | + // source code. |
| 119 | + psym eq other |
| 120 | + |
| 121 | + /** Is this a class symbol? Also returns true for refinements |
| 122 | + * since they get a class symbol in Scala 2. |
| 123 | + */ |
| 124 | + def isClass: Boolean = psym match |
| 125 | + case tp: Scala2RefinedType => |
| 126 | + true |
| 127 | + case tp: StructuralRef => |
| 128 | + false |
| 129 | + case sym: Symbol => |
| 130 | + sym.isClass |
| 131 | + |
| 132 | + /** Is this a trait symbol? */ |
| 133 | + def isTrait: Boolean = psym match |
| 134 | + case tp: Scala2RefinedType => |
| 135 | + false |
| 136 | + case tp: StructuralRef => |
| 137 | + false |
| 138 | + case sym: Symbol => |
| 139 | + sym.is(Trait) |
| 140 | + |
| 141 | + /** An emulation of `Symbol#isNonBottomSubClass` from Scala 2. |
| 142 | + * |
| 143 | + * The documentation of the original method is: |
| 144 | + * |
| 145 | + * > Is this class symbol a subclass of that symbol, |
| 146 | + * > and is this class symbol also different from Null or Nothing? |
| 147 | + * |
| 148 | + * Which sounds fine, except that it is also used with non-class symbols, |
| 149 | + * so what does it do then? Its implementation delegates to `Type#baseTypeSeq` |
| 150 | + * whose documentation states: |
| 151 | + * |
| 152 | + * > The base type sequence of T is the smallest set of [...] class types Ti, so that [...] |
| 153 | + * |
| 154 | + * But this is also wrong: the sequence returned by `baseTypeSeq` can |
| 155 | + * contain non-class symbols. |
| 156 | + * |
| 157 | + * Given that we cannot rely on the documentation and that the |
| 158 | + * implementation is extremely complex, this reimplementation is mostly |
| 159 | + * based on reverse-engineering rules derived from the observed behavior of |
| 160 | + * the original method. |
| 161 | + */ |
| 162 | + def isNonBottomSubClass(that: PseudoSymbol): Boolean = |
| 163 | + /** Recurse on the upper-bound of `psym`: an abstract type is a sub of a |
| 164 | + * pseudo-symbol, if its upper-bound is a sub of that pseudo-symbol. |
| 165 | + */ |
| 166 | + def goUpperBound(psym: Symbol | StructuralRef): Boolean = |
| 167 | + val info = psym match |
| 168 | + case sym: Symbol => sym.info |
| 169 | + case tp: StructuralRef => tp.info |
| 170 | + info match |
| 171 | + case info: TypeBounds => |
| 172 | + go(pseudoSymbol(info.hi)) |
| 173 | + case _ => |
| 174 | + false |
| 175 | + |
| 176 | + def go(psym: PseudoSymbol): Boolean = |
| 177 | + psym.sameSymbol(that) || |
| 178 | + // As mentioned in the documentation of `Scala2RefinedType`, in Scala 2 |
| 179 | + // these types get their own unique synthetic class symbol, therefore they |
| 180 | + // don't have any sub-class Note that we must return false even if the lhs |
| 181 | + // is an abstract type upper-bounded by this refinement, since each |
| 182 | + // textual appearance of a refinement will have its own class symbol. |
| 183 | + !that.isInstanceOf[Scala2RefinedType] && |
| 184 | + psym.match |
| 185 | + case sym1: Symbol => that match |
| 186 | + case sym2: Symbol => |
| 187 | + if sym1.isClass && sym2.isClass then |
| 188 | + sym1.derivesFrom(sym2) |
| 189 | + else if !sym1.isClass then |
| 190 | + goUpperBound(sym1) |
| 191 | + else |
| 192 | + // sym2 is an abstract type, return false because |
| 193 | + // `isNonBottomSubClass` in Scala 2 never considers a class C to |
| 194 | + // be a a sub of an abstract type T, even if it was declared as |
| 195 | + // `type T >: C`. |
| 196 | + false |
| 197 | + case _ => |
| 198 | + goUpperBound(sym1) |
| 199 | + case tp1: StructuralRef => |
| 200 | + goUpperBound(tp1) |
| 201 | + case tp1: RefinedType => |
| 202 | + go(pseudoSymbol(tp1.parent)) |
| 203 | + case AndType(tp11, tp12) => |
| 204 | + go(pseudoSymbol(tp11)) || go(pseudoSymbol(tp12)) |
| 205 | + end go |
| 206 | + |
| 207 | + go(psym) |
| 208 | + end isNonBottomSubClass |
| 209 | + end extension |
| 210 | + |
| 211 | + /** An emulation of `Erasure#intersectionDominator` from Scala 2. |
| 212 | + * |
| 213 | + * Accurately reproducing the behavior of this method is extremely difficult |
| 214 | + * because it operates on the symbols of the _non-erased_ parent types, an |
| 215 | + * implementation detail of the compiler. Furthermore, these non-class |
| 216 | + * symbols are passed to methods such as `isNonBottomSubClass` whose behavior |
| 217 | + * is only specified for class symbols. Therefore, the accuracy of this |
| 218 | + * method cannot be guaranteed, the best we can do is make sure it works on |
| 219 | + * as many test cases as possible which can be run from sbt using: |
| 220 | + * > sbt-dotty/scripted scala2-compat/erasure |
| 221 | + * |
| 222 | + * The body of this method is made to look as much as the Scala 2 version as |
| 223 | + * possible to make them easier to compare, cf: |
| 224 | + * https://github.com/scala/scala/blob/v2.13.5/src/reflect/scala/reflect/internal/transform/Erasure.scala#L356-L389 |
| 225 | + */ |
| 226 | + def intersectionDominator(parents: List[Type])(using Context): Type = |
| 227 | + val psyms = parents.map(pseudoSymbol) |
| 228 | + if (psyms.contains(defn.ArrayClass)) { |
| 229 | + defn.ArrayOf( |
| 230 | + intersectionDominator(parents.collect { case defn.ArrayOf(arg) => arg })) |
| 231 | + } else { |
| 232 | + def isUnshadowed(psym: PseudoSymbol) = |
| 233 | + !(psyms.exists(qsym => !psym.sameSymbol(qsym) && qsym.isNonBottomSubClass(psym))) |
| 234 | + val cs = parents.iterator.filter { p => |
| 235 | + val psym = pseudoSymbol(p) |
| 236 | + psym.isClass && !psym.isTrait && isUnshadowed(psym) |
| 237 | + } |
| 238 | + (if (cs.hasNext) cs else parents.iterator.filter(p => isUnshadowed(pseudoSymbol(p)))).next() |
| 239 | + } |
| 240 | + |
| 241 | + /** A flattened list of parents of this intersection. |
| 242 | + * |
| 243 | + * Mimic what Scala 2 does: intersections like `A with (B with C)` are |
| 244 | + * flattened to three parents. |
| 245 | + */ |
| 246 | + def flattenedParents(tp: AndType)(using Context): List[Type] = |
| 247 | + val parents = ListBuffer[Type]() |
| 248 | + |
| 249 | + def collect(parent: Type, parents: ListBuffer[Type]): Unit = parent.dealiasKeepAnnots match |
| 250 | + case AndType(tp1, tp2) => |
| 251 | + collect(tp1, parents) |
| 252 | + collect(tp2, parents) |
| 253 | + case _ => |
| 254 | + supportedType(parent) |
| 255 | + parents += parent |
| 256 | + |
| 257 | + collect(tp.tp1, parents) |
| 258 | + collect(tp.tp2, parents) |
| 259 | + parents.toList |
| 260 | + end flattenedParents |
| 261 | +end Scala2Erasure |
0 commit comments