Skip to content

Commit e21623f

Browse files
committed
Treat Scala.js pseudo-unions in Scala 2 symbols as real unions
When unpickling a Scala 2 symbol, we now treat `scala.scalajs.js.|[A, B]` as if it were a real `A | B`, this requires a special-case in erasure to emit the correct signatures in SJSIR. Unresolved issues: - The companion object of `js.|` defines implicit conversions like `undefOr2ops`, those are no longer automatically in scope for values with a union type (which breaks many JUnitTests). Should we add a special-case in typer to handle this or can this be fixed on the scalajs-library side? - When compiling Scala.js code from source, `A | B` is interpreted as `js.|[A, B]` if `js.|` is in scope (e.g. via an import), should we just ignore that import in typer?
1 parent dfa1b6b commit e21623f

File tree

7 files changed

+89
-8
lines changed

7 files changed

+89
-8
lines changed

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Phases._
66
import Flags.JavaDefined
77
import Uniques.unique
88
import TypeOps.makePackageObjPrefixExplicit
9+
import backend.sjs.JSDefinitions
910
import transform.ExplicitOuter._
1011
import transform.ValueClasses._
1112
import transform.TypeUtils._
@@ -570,14 +571,37 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
570571
else defn.TupleXXLClass.typeRef
571572
}
572573

573-
/** The erasure of a symbol's info. This is different from `apply` in the way `ExprType`s and
574-
* `PolyType`s are treated. `eraseInfo` maps them them to method types, whereas `apply` maps them
575-
* to the underlying type.
574+
/** The erasure of a symbol's info. This is different from `apply` in several ways:
575+
* - `ExprType` and `PolyType` are erased to method types, whereas `apply` erases them
576+
* to the underlying type.
577+
* - context functions are flattened (see `Config.flattenContextFunctionResults`)
578+
* - Unions in Scala2.js symbols are erased to the Scala.js pseudo-union type.
576579
*/
577580
def eraseInfo(tp: Type, sym: Symbol)(using Context): Type =
578-
val tp1 = tp match
581+
val tp0 = tp match
579582
case tp: MethodicType => integrateContextResults(tp, contextResultCount(sym))
580583
case _ => tp
584+
585+
// In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were
586+
// real unions, but we must still erase them as Scala 2 would to emit
587+
// the correct signatures in SJSIR.
588+
// This logic must be in `eraseInfo` and not `apply`, because a Scala 2 type
589+
// which is not the info of a symbol could contain a Scala 3 union after
590+
// substitution (e.g. via `asSeenFrom`), and we do not want to erase these
591+
// unions specially since it could affect the behavior of
592+
// `Denotation#matches`.
593+
val tp1 =
594+
if sourceLanguage.isScala2 && ctx.settings.scalajs.value then
595+
val eraseAsPseudoUnion = new TypeMap:
596+
def apply(tp: Type) = tp match
597+
case tp: OrType =>
598+
JSDefinitions.jsdefn.PseudoUnionType
599+
case tp =>
600+
mapOver(tp)
601+
eraseAsPseudoUnion(tp0)
602+
else
603+
tp0
604+
581605
tp1 match
582606
case ExprType(rt) =>
583607
if sym.is(Param) then apply(tp1)

compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import NameKinds.{Scala2MethodNameKinds, SuperAccessorName, ExpandedName}
1313
import util.Spans._
1414
import dotty.tools.dotc.ast.{tpd, untpd}, ast.tpd._
1515
import ast.untpd.Modifiers
16+
import backend.sjs.JSDefinitions
1617
import printing.Texts._
1718
import printing.Printer
1819
import io.AbstractFile
@@ -674,6 +675,10 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
674675

675676
def removeSingleton(tp: Type): Type =
676677
if (tp isRef defn.SingletonClass) defn.AnyType else tp
678+
def mapArg(arg: Type) = arg match {
679+
case arg: TypeRef if isBound(arg) => arg.symbol.info
680+
case _ => arg
681+
}
677682
def elim(tp: Type): Type = tp match {
678683
case tp @ RefinedType(parent, name, rinfo) =>
679684
val parent1 = elim(tp.parent)
@@ -689,12 +694,11 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
689694
}
690695
case tp @ AppliedType(tycon, args) =>
691696
val tycon1 = tycon.safeDealias
692-
def mapArg(arg: Type) = arg match {
693-
case arg: TypeRef if isBound(arg) => arg.symbol.info
694-
case _ => arg
695-
}
696697
if (tycon1 ne tycon) elim(tycon1.appliedTo(args))
697698
else tp.derivedAppliedType(tycon, args.map(mapArg))
699+
case tp: AndOrType =>
700+
// scalajs.js.|.UnionOps has a type parameter upper-bounded by `_ | _`
701+
tp.derivedAndOrType(mapArg(tp.tp1).bounds.hi, mapArg(tp.tp2).bounds.hi)
698702
case _ =>
699703
tp
700704
}
@@ -776,6 +780,12 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
776780
val tycon = select(pre, sym)
777781
val args = until(end, () => readTypeRef())
778782
if (sym == defn.ByNameParamClass2x) ExprType(args.head)
783+
else if (ctx.settings.scalajs.value && args.length == 2 &&
784+
sym.owner == JSDefinitions.jsdefn.ScalaJSJSPackageClass && sym == JSDefinitions.jsdefn.PseudoUnionClass) {
785+
// Treat Scala.js pseudo-unions as real unions, this requires a
786+
// special-case in erasure, see TypeErasure#eraseInfo.
787+
OrType(args(0), args(1), soft = false)
788+
}
779789
else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly)))
780790
else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams)
781791
else tycon
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
lazy val scala2Lib = project.in(file("scala2Lib"))
2+
.enablePlugins(ScalaJSPlugin)
3+
.settings(
4+
// TODO: switch to 2.13.5 once we've upgrade sbt-scalajs to 1.5.0
5+
scalaVersion := "2.13.4"
6+
)
7+
8+
lazy val dottyApp = project.in(file("dottyApp"))
9+
.dependsOn(scala2Lib)
10+
.enablePlugins(ScalaJSPlugin)
11+
.settings(
12+
scalaVersion := sys.props("plugin.scalaVersion"),
13+
14+
scalaJSUseMainModuleInitializer := true,
15+
scalaJSLinkerConfig ~= (_.withCheckIR(true)),
16+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
object Main {
2+
def main(args: Array[String]): Unit = {
3+
val a = new scala2Lib.A
4+
assert(a.foo(1) == "1")
5+
assert(a.foo("") == "1")
6+
assert(a.foo(Array(1)) == "2")
7+
8+
val b = new scala2Lib.B
9+
assert(b.foo(1) == "1")
10+
assert(b.foo("") == "1")
11+
assert(b.foo(Array(1)) == "2")
12+
}
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version"))
2+
addSbtPlugin("org.scala-js" % "sbt-scalajs" % sys.props("plugin.scalaJSVersion"))
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Keep synchronized with dottyApp/Api.scala
2+
package scala2Lib
3+
4+
import scala.scalajs.js
5+
import js.|
6+
7+
class A {
8+
def foo(x: Int | String): String = "1"
9+
def foo(x: Array[Int]): String = "2"
10+
}
11+
12+
class B extends js.Object {
13+
def foo(x: Int | String): String = "1"
14+
def foo(x: Array[Int]): String = "2"
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
> dottyApp/run

0 commit comments

Comments
 (0)