From a5d6d8fbdc44c9b3c0e928969de1616b65ec284e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 16 Mar 2021 16:35:31 +0100 Subject: [PATCH 1/9] Fix a typo in an error message Also remove parens because we ended up with "... since the scrutinee type ((item: Int))" when the scrutinee is a singleton type. --- compiler/src/dotty/tools/dotc/reporting/messages.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index dab952004325..7c17cf198d57 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2126,7 +2126,7 @@ import transform.SymUtils._ val addendum = if (scrutTp != testTp) s" is a subtype of ${testTp.show}" else " is the same as the tested type" - s"The highlighted type test will always succeed since the scrutinee type ($scrutTp.show)" + addendum + s"The highlighted type test will always succeed since the scrutinee type ${scrutTp.show}" + addendum } def explain = "" } From 57e60acaa736d9a4efe3beb9826c4ad49632d26e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 9 Mar 2021 15:27:52 +0100 Subject: [PATCH 2/9] Use 2.13.5 for all scala2-compat tests --- sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt index 1582924e0f41..8a92cf6c95c8 100644 --- a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt @@ -1,6 +1,6 @@ lazy val scala2Lib = project.in(file("scala2Lib")) .settings( - scalaVersion := "2.13.2" + scalaVersion := "2.13.5" ) lazy val dottyApp = project.in(file("dottyApp")) From 9fc91d86f3b1bdac85ae6ca44a4a7ebd7bf5c810 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 9 Mar 2021 15:34:34 +0100 Subject: [PATCH 3/9] Remove workaround for fixed sbt issue --- sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt | 7 +------ sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt | 6 +----- sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt | 6 +----- sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt | 6 +----- sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt | 6 +----- 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt index 8a92cf6c95c8..794d78b308c0 100644 --- a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt @@ -6,10 +6,5 @@ lazy val scala2Lib = project.in(file("scala2Lib")) lazy val dottyApp = project.in(file("dottyApp")) .dependsOn(scala2Lib) .settings( - scalaVersion := sys.props("plugin.scalaVersion"), - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := sys.props("plugin.scalaVersion") ) - diff --git a/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt index 4f4ae373d2b1..27c7d9ea9b3c 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt @@ -7,9 +7,5 @@ lazy val lib = (project in file ("lib")) lazy val test = (project in file ("main")) .dependsOn(lib) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt index 2ed3b7a520b9..6a579f2be966 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt @@ -7,9 +7,5 @@ lazy val `i8847-lib` = (project in file ("lib")) lazy val `i8847-test` = (project in file ("main")) .dependsOn(`i8847-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt index 733b45e1b379..1f5c07900161 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt @@ -7,9 +7,5 @@ lazy val `i9916a-lib` = (project in file ("lib")) lazy val `i9916a-test` = (project in file ("main")) .dependsOn(`i9916a-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt index 9a4e81163911..f597a8610607 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt @@ -7,9 +7,5 @@ lazy val `i9916b-lib` = (project in file ("lib")) lazy val `i9916b-test` = (project in file ("main")) .dependsOn(`i9916b-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) From 65df24d5c590a2c5614c7880fe168dab0f2f6859 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 16 Mar 2021 16:43:08 +0100 Subject: [PATCH 4/9] Add TypeErasure#isSymbol, true when erasing a symbol info This will be used in the next commit to deal with Scala.js unions. Also change the erasure of value classes to preserve flags like `isSymbol` when erasing its underlying type. --- .../dotty/tools/dotc/core/TypeErasure.scala | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index ccf344fa8b49..9426ec0850d8 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -142,29 +142,31 @@ object TypeErasure { } } - private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) = + private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) = extension (b: Boolean) def toInt = if b then 1 else 0 wildcardOK.toInt - + (isConstructor.toInt << 1) - + (semiEraseVCs.toInt << 2) - + (sourceLanguage.ordinal << 3) + + (isSymbol.toInt << 1) + + (isConstructor.toInt << 2) + + (semiEraseVCs.toInt << 3) + + (sourceLanguage.ordinal << 4) - private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 3)) + private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 4)) for sourceLanguage <- SourceLanguage.values semiEraseVCs <- List(false, true) isConstructor <- List(false, true) + isSymbol <- List(false, true) wildcardOK <- List(false, true) do - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) = - new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK) + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) = + new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK) /** Produces an erasure function. See the documentation of the class [[TypeErasure]] * for a description of each parameter. */ - private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure = - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) + private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean): TypeErasure = + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) /** The current context with a phase no later than erasure */ def preErasureCtx(using Context) = @@ -175,7 +177,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -183,11 +185,11 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** The erasure that Scala 2 would use for this type. */ def scala2Erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -197,7 +199,7 @@ object TypeErasure { def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = { val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava) - val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, wildcardOK = true) + val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, isSymbol = false, wildcardOK = true) erase.sigName(normTp)(using preErasureCtx) } @@ -227,7 +229,7 @@ object TypeErasure { def transformInfo(sym: Symbol, tp: Type)(using Context): Type = { val sourceLanguage = SourceLanguage(sym) val semiEraseVCs = !sourceLanguage.isJava // Java sees our value classes as regular classes. - val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, wildcardOK = false) + val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, isSymbol = true, wildcardOK = false) def eraseParamBounds(tp: PolyType): Type = tp.derivedLambdaType( @@ -446,10 +448,11 @@ import TypeErasure._ * (they will be fully erased in [[ElimErasedValueType]]). * If false, they are erased like normal classes. * @param isConstructor Argument forms part of the type of a constructor + * @param isSymbol If true, the type being erased is the info of a symbol. * @param wildcardOK Wildcards are acceptable (true when using the erasure * for computing a signature name). */ -class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) { +class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) { /** The erasure |T| of a type T. This is: * @@ -523,7 +526,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)(tpToErase) + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)(tpToErase) val (names, formals0) = if (tp.isErasedMethod) (Nil, Nil) else (tp.paramNames, tp.paramInfos) val formals = formals0.mapConserve(paramErasure) eraseResult(tp.resultType) match { @@ -567,7 +570,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val defn.ArrayOf(elemtp) = tp if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType) else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType - else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) + else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK)(elemtp)) } private def erasePair(tp: Type)(using Context): Type = { @@ -608,7 +611,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val genericUnderlying = unbox.info.resultType val underlying = tp.select(unbox).widen.resultType - val erasedUnderlying = erasure(underlying) + // The underlying part of an ErasedValueType cannot be an ErasedValueType itself + val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK) + val erasedUnderlying = erase(underlying) // Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to // be binary-compatible with Scala 2 we need two special cases for polymorphic @@ -646,7 +651,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a // constructor method should not be semi-erased. if semiEraseVCs && isConstructor && !tp.isInstanceOf[MethodOrPoly] then - erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp) + erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK).eraseResult(tp) else tp match case tp: TypeRef => val sym = tp.symbol From 7feba77c0efe0eee9fa65914cc006eae28980727 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 9 Mar 2021 15:00:27 +0100 Subject: [PATCH 5/9] 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. Remaining issues after this commit fixed in later commits of this PR: - 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). - 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). Additionally, a few tests had to be disabled until we can depend on their fixed upstream version (https://github.com/scala-js/scala-js/pull/4451). --- .../dotty/tools/dotc/core/TypeErasure.scala | 15 ++++++++++++++- .../core/unpickleScala2/Scala2Unpickler.scala | 18 ++++++++++++++---- project/Build.scala | 5 ++++- .../scala2-compat/erasure-scalajs/build.sbt | 16 ++++++++++++++++ .../erasure-scalajs/dottyApp/Main.scala | 13 +++++++++++++ .../erasure-scalajs/project/plugins.sbt | 2 ++ .../erasure-scalajs/scala2Lib/Api.scala | 15 +++++++++++++++ .../scala2-compat/erasure-scalajs/test | 1 + .../js-trait-members-wrong-kind.check | 6 +++--- 9 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9426ec0850d8..707ed053f02a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -6,6 +6,7 @@ import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Phases._ import Flags.JavaDefined import Uniques.unique import TypeOps.makePackageObjPrefixExplicit +import backend.sjs.JSDefinitions import transform.ExplicitOuter._ import transform.ValueClasses._ import transform.TypeUtils._ @@ -523,7 +524,19 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava) case OrType(tp1, tp2) => - TypeComparer.orType(this(tp1), this(tp2), isErased = true) + if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then + // In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were + // real unions, but we must still erase them as Scala 2 would to emit + // the correct signatures in SJSIR. + // We only do this when `isSymbol` is true since in other situations we + // cannot distinguish a Scala.js pseudo-union from a Scala 3 union that + // has been substituted into a Scala 2 type (e.g., via `asSeenFrom`), + // erasing these unions as if they were pseudo-unions could have an + // impact on overriding relationships so it's best to leave them + // alone (and this doesn't impact the SJSIR we generate). + JSDefinitions.jsdefn.PseudoUnionType + else + TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)(tpToErase) diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 7738bda6a800..54a7ad91e434 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -13,6 +13,7 @@ import NameKinds.{Scala2MethodNameKinds, SuperAccessorName, ExpandedName} import util.Spans._ import dotty.tools.dotc.ast.{tpd, untpd}, ast.tpd._ import ast.untpd.Modifiers +import backend.sjs.JSDefinitions import printing.Texts._ import printing.Printer import io.AbstractFile @@ -675,6 +676,10 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas def removeSingleton(tp: Type): Type = if (tp isRef defn.SingletonClass) defn.AnyType else tp + def mapArg(arg: Type) = arg match { + case arg: TypeRef if isBound(arg) => arg.symbol.info + case _ => arg + } def elim(tp: Type): Type = tp match { case tp @ RefinedType(parent, name, rinfo) => val parent1 = elim(tp.parent) @@ -690,12 +695,11 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas } case tp @ AppliedType(tycon, args) => val tycon1 = tycon.safeDealias - def mapArg(arg: Type) = arg match { - case arg: TypeRef if isBound(arg) => arg.symbol.info - case _ => arg - } if (tycon1 ne tycon) elim(tycon1.appliedTo(args)) else tp.derivedAppliedType(tycon, args.map(mapArg)) + case tp: AndOrType => + // scalajs.js.|.UnionOps has a type parameter upper-bounded by `_ | _` + tp.derivedAndOrType(mapArg(tp.tp1).bounds.hi, mapArg(tp.tp2).bounds.hi) case _ => tp } @@ -777,6 +781,12 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas val tycon = select(pre, sym) val args = until(end, () => readTypeRef()) if (sym == defn.ByNameParamClass2x) ExprType(args.head) + else if (ctx.settings.scalajs.value && args.length == 2 && + sym.owner == JSDefinitions.jsdefn.ScalaJSJSPackageClass && sym == JSDefinitions.jsdefn.PseudoUnionClass) { + // Treat Scala.js pseudo-unions as real unions, this requires a + // special-case in erasure, see TypeErasure#eraseInfo. + OrType(args(0), args(1), soft = false) + } else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon diff --git a/project/Build.scala b/project/Build.scala index e5aa08994797..4168046a8369 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1105,9 +1105,12 @@ object Build { -- "ObjectTest.scala" // compile errors caused by #9588 -- "StackTraceTest.scala" // would require `npm install source-map-support` -- "UnionTypeTest.scala" // requires the Scala 2 macro defined in Typechecking*.scala + -- "PromiseMock.scala" -- "AsyncTest.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in )).get - ++ (dir / "js/src/test/require-2.12" ** "*.scala").get + ++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter) + -- "JSOptionalTest212.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in + )).get ++ (dir / "js/src/test/require-sam" ** "*.scala").get ++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get ) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt new file mode 100644 index 000000000000..4186b8167e93 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt @@ -0,0 +1,16 @@ +lazy val scala2Lib = project.in(file("scala2Lib")) + .enablePlugins(ScalaJSPlugin) + .settings( + // TODO: switch to 2.13.5 once we've upgrade sbt-scalajs to 1.5.0 + scalaVersion := "2.13.4" + ) + +lazy val dottyApp = project.in(file("dottyApp")) + .dependsOn(scala2Lib) + .enablePlugins(ScalaJSPlugin) + .settings( + scalaVersion := sys.props("plugin.scalaVersion"), + + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withCheckIR(true)), + ) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala new file mode 100644 index 000000000000..47774c732650 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala @@ -0,0 +1,13 @@ +object Main { + def main(args: Array[String]): Unit = { + val a = new scala2Lib.A + assert(a.foo(1) == "1") + assert(a.foo("") == "1") + assert(a.foo(Array(1)) == "2") + + val b = new scala2Lib.B + assert(b.foo(1) == "1") + assert(b.foo("") == "1") + assert(b.foo(Array(1)) == "2") + } +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt new file mode 100644 index 000000000000..f69df9c39cc7 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version")) +addSbtPlugin("org.scala-js" % "sbt-scalajs" % sys.props("plugin.scalaJSVersion")) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala new file mode 100644 index 000000000000..5c7388496f59 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala @@ -0,0 +1,15 @@ +// Keep synchronized with dottyApp/Api.scala +package scala2Lib + +import scala.scalajs.js +import js.| + +class A { + def foo(x: Int | String): String = "1" + def foo(x: Array[Int]): String = "2" +} + +class B extends js.Object { + def foo(x: Int | String): String = "1" + def foo(x: Array[Int]): String = "2" +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test new file mode 100644 index 000000000000..a67688f8dc37 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test @@ -0,0 +1 @@ +> dottyApp/run diff --git a/tests/neg-scalajs/js-trait-members-wrong-kind.check b/tests/neg-scalajs/js-trait-members-wrong-kind.check index e6c20f1b2cce..d76b37b84680 100644 --- a/tests/neg-scalajs/js-trait-members-wrong-kind.check +++ b/tests/neg-scalajs/js-trait-members-wrong-kind.check @@ -2,15 +2,15 @@ 5 | lazy val a1: js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | A non-native JS trait cannot contain lazy vals --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:7:29 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:7:32 ----------------------------------------------------- 7 | def a(): js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:8:35 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:8:38 ----------------------------------------------------- 8 | def b(x: Int): js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:9:26 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:9:29 ----------------------------------------------------- 9 | def c_=(v: Int): Unit = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. From de1b04ecb91a31d2b84b1ffce9036e9d1fb53c95 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 9 Mar 2021 19:33:39 +0100 Subject: [PATCH 6/9] Ignore imports of js.| This fixes various compilation errors with sjsJUnitTests after the previous commit. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a8a638d365d2..85780105840c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import ast._ import Trees._ @@ -219,7 +220,13 @@ class Typer extends Namer denot = denot.filterWithPredicate { mbr => mbr.matchesImportBound(if mbr.symbol.is(Given) then imp.givenBound else imp.wildcardBound) } - if reallyExists(denot) then + def isScalaJsPseudoUnion = + denot.name == tpnme.raw.BAR && ctx.settings.scalajs.value && denot.symbol == JSDefinitions.jsdefn.PseudoUnionClass + // Just like Scala2Unpickler reinterprets Scala.js pseudo-unions + // as real union types, we want references to `A | B` in sources + // to be typed as a real union even if `js.|` has been imported, + // so we ignore that import. + if reallyExists(denot) && !isScalaJsPseudoUnion then if unimported.isEmpty || !unimported.contains(pre.termSymbol) then return pre.select(name, denot) case _ => From f55d869fa5ee36b912a5a7b6b70c042f8069f8a0 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 16 Mar 2021 16:27:22 +0100 Subject: [PATCH 7/9] Support Scala.js unions extra methods via implicit conversion The companion of `js.|` contains the following widely used implicit conversion to a value class: /** Provides an [[Option]]-like API to [[js.UndefOr]]. */ implicit def undefOr2ops[A](value: js.UndefOr[A]): js.UndefOrOps[A] = new js.UndefOrOps(value) (where `UndefOr[A]` dealiases to `A | Unit`). Since we re-interpret Scala.js unions as real unions, this companion is not in the implicit scope of `A | Unit`, we work around this by injecting a new `UnitOps` with the implicit conversion we want in the implicit scope of `Unit` (we could have directly injected the object `js.|`, but that contains other implicits we do not need). This finally lets us compile and run the sjsJUnitTests after the previous two commits. --- compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala | 2 ++ compiler/src/dotty/tools/dotc/typer/Implicits.scala | 9 +++++++++ library-js/src/scala/scalajs/js/internal/UnitOps.scala | 8 ++++++++ project/Build.scala | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 library-js/src/scala/scalajs/js/internal/UnitOps.scala diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 4025446c8cd8..0322e56b2f41 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -59,6 +59,8 @@ final class JSDefinitions()(using Context) { @threadUnsafe lazy val PseudoUnion_fromTypeConstructorR = PseudoUnionModule.requiredMethodRef("fromTypeConstructor") def PseudoUnion_fromTypeConstructor(using Context) = PseudoUnion_fromTypeConstructorR.symbol + @threadUnsafe lazy val UnionOpsModuleRef = requiredModuleRef("scala.scalajs.js.internal.UnitOps") + @threadUnsafe lazy val JSArrayType: TypeRef = requiredClassRef("scala.scalajs.js.Array") def JSArrayClass(using Context) = JSArrayType.symbol.asClass @threadUnsafe lazy val JSDynamicType: TypeRef = requiredClassRef("scala.scalajs.js.Dynamic") diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 2969d7f4804f..ffb1d403230a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import ast.{Trees, TreeTypeMap, untpd, tpd, DesugarEnums} import util.Spans._ @@ -634,6 +635,14 @@ trait ImplicitRunInfo: else pre.member(sym.name.toTermName) .suchThat(companion => companion.is(Module) && companion.owner == sym.owner) .symbol) + + // The companion of `js.|` defines an implicit conversions from + // `A | Unit` to `js.UndefOrOps[A]`. To keep this conversion in scope + // in Scala 3, where we re-interpret `js.|` as a real union, we inject + // it in the scope of `Unit`. + if t.isRef(defn.UnitClass) && ctx.settings.scalajs.value then + companions += JSDefinitions.jsdefn.UnionOpsModuleRef + if sym.isClass then for p <- t.parents do companions ++= iscopeRefs(p) else diff --git a/library-js/src/scala/scalajs/js/internal/UnitOps.scala b/library-js/src/scala/scalajs/js/internal/UnitOps.scala new file mode 100644 index 000000000000..e2abc2f83c7e --- /dev/null +++ b/library-js/src/scala/scalajs/js/internal/UnitOps.scala @@ -0,0 +1,8 @@ +package scala.scalajs.js.internal + +import scala.scalajs.js + +/** Under -scalajs, this object is part of the implicit scope of `scala.Unit` */ +object UnitOps: + implicit def unitOrOps[A](x: A | Unit): js.UndefOrOps[A] = + new js.UndefOrOps(x) diff --git a/project/Build.scala b/project/Build.scala index 4168046a8369..40541849ac66 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -698,7 +698,7 @@ object Build { settings( libraryDependencies += ("org.scala-js" %% "scalajs-library" % scalaJSVersion).withDottyCompat(scalaVersion.value), - Compile / unmanagedSourceDirectories := + Compile / unmanagedSourceDirectories ++= (`scala3-library-bootstrapped` / Compile / unmanagedSourceDirectories).value, // Configure the source maps to point to GitHub for releases From f1b5f024ddb55e03874a88870f8ec32bbc055324 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 16 Mar 2021 18:27:44 +0100 Subject: [PATCH 8/9] sjsJUnitTest: copy-paste PromiseMock from scala.js upstream To improve our testing of unions until we get a release of Scala.js with the fixed PromiseMock. This also lets us re-enable AsyncTest which depends on PromiseMock. --- project/Build.scala | 3 +- .../testsuite/jsinterop/PromiseMock.scala | 260 ++++++++++++++++++ 2 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala diff --git a/project/Build.scala b/project/Build.scala index 40541849ac66..3a4816737e7b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1105,7 +1105,8 @@ object Build { -- "ObjectTest.scala" // compile errors caused by #9588 -- "StackTraceTest.scala" // would require `npm install source-map-support` -- "UnionTypeTest.scala" // requires the Scala 2 macro defined in Typechecking*.scala - -- "PromiseMock.scala" -- "AsyncTest.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in + -- "PromiseMock.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in + // and remove copy in tests/sjs-junit )).get ++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter) diff --git a/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala new file mode 100644 index 000000000000..6764cdc7ac83 --- /dev/null +++ b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala @@ -0,0 +1,260 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.jsinterop + +import scala.scalajs.js +import scala.scalajs.js.annotation._ +import scala.scalajs.js.| + +import js.Thenable + +object PromiseMock { + + @noinline + def withMockedPromise[A](body: (() => Unit) => A): A = { + val global = org.scalajs.testsuite.utils.JSUtils.globalObject + + val oldPromise = + if (global.hasOwnProperty("Promise").asInstanceOf[Boolean]) Some(global.Promise) + else None + + global.Promise = js.constructorOf[MockPromise[_]] + try { + body(MockPromise.processQueue _) + } finally { + oldPromise.fold { + js.special.delete(global, "Promise") + } { old => + global.Promise = old + } + } + } + + @noinline + def withMockedPromiseIfExists[A](body: (Option[() => Unit]) => A): A = { + val global = org.scalajs.testsuite.utils.JSUtils.globalObject + + val oldPromise = global.Promise + + if (js.isUndefined(oldPromise)) { + body(None) + } else { + global.Promise = js.constructorOf[MockPromise[_]] + try { + body(Some(MockPromise.processQueue _)) + } finally { + global.Promise = oldPromise + } + } + } + + private object MockPromise { + private val queue = js.Array[js.Function0[Any]]() + + @JSExportStatic + def resolve[A](value: A | js.Thenable[A]): MockPromise[A] = { + new MockPromise[A]({ + (resolve: js.Function1[A | js.Thenable[A], _], + reject: js.Function1[Any, _]) => + resolve(value) + }) + } + + @JSExportStatic + def reject(reason: Any): MockPromise[Nothing] = { + new MockPromise[Nothing]({ + (resolve: js.Function1[Nothing | js.Thenable[Nothing], _], + reject: js.Function1[Any, _]) => + reject(reason) + }) + } + + def enqueue(f: js.Function0[Any]): Unit = + queue.push(f) + + def processQueue(): Unit = { + while (queue.nonEmpty) + queue.shift()() + } + + private sealed abstract class State[+A] + + private case object Pending extends State[Nothing] + private case class Fulfilled[+A](value: A) extends State[A] + private case class Rejected(reason: Any) extends State[Nothing] + + private def isNotAnObject(x: Any): Boolean = x match { + case null | () | _:Double | _:Boolean | _:String => true + case _ => false + } + + private def isCallable(x: Any): Boolean = + js.typeOf(x.asInstanceOf[js.Any]) == "function" + + private def throwAny(e: Any): Nothing = { + throw (e match { + case th: Throwable => th + case _ => js.JavaScriptException(e) + }) + } + + private def tryCatchAny[A](tryBody: => A)(catchBody: Any => A): A = { + try { + tryBody + } catch { + case th: Throwable => + catchBody(th match { + case js.JavaScriptException(e) => e + case _ => th + }) + } + } + } + + private class MockPromise[+A]( + executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _]) + extends js.Object with js.Thenable[A] { + + import MockPromise._ + + private[this] var state: State[A] = Pending + + private[this] var fulfillReactions = js.Array[js.Function1[A, Any]]() + private[this] var rejectReactions = js.Array[js.Function1[Any, Any]]() + + init(executor) + + // 25.4.3.1 Promise(executor) + private[this] def init( + executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _]) = { + tryCatchAny[Unit] { + executor(resolve _, reject _) + } { e => + reject(e) + } + } + + private[this] def fulfill(value: A): Unit = { + assert(state == Pending) + state = Fulfilled(value) + clearAndTriggerReactions(fulfillReactions, value) + } + + private[this] def clearAndTriggerReactions[A]( + reactions: js.Array[js.Function1[A, Any]], + argument: A): Unit = { + + assert(state != Pending) + + fulfillReactions = null + rejectReactions = null + + for (reaction <- reactions) + enqueue(() => reaction(argument)) + } + + // 25.4.1.3.2 Promise Resolve Functions + private[this] def resolve(resolution: A | Thenable[A]): Unit = { + if (state == Pending) { + if (resolution.asInstanceOf[AnyRef] eq this) { + reject(new js.TypeError("Self resolution")) + } else if (isNotAnObject(resolution)) { + fulfill(resolution.asInstanceOf[A]) + } else { + tryCatchAny { + val thenAction = resolution.asInstanceOf[js.Dynamic].`then` + if (!isCallable(thenAction)) { + fulfill(resolution.asInstanceOf[A]) + } else { + val thenable = resolution.asInstanceOf[Thenable[A]] + val thenActionFun = thenAction.asInstanceOf[js.Function] + enqueue(() => promiseResolveThenableJob(thenable, thenActionFun)) + } + } { e => + reject(e) + } + } + } + } + + // 25.4.2.2 PromiseResolveThenableJob + private[this] def promiseResolveThenableJob(thenable: Thenable[A], + thenAction: js.Function): Unit = { + thenAction.call(thenable, resolve _, reject _) + } + + // 25.4.1.3.1 Promise Reject Functions + private[this] def reject(reason: Any): Unit = { + if (state == Pending) { + state = Rejected(reason) + clearAndTriggerReactions(rejectReactions, reason) + } + } + + // 25.4.5.3 Promise.prototype.then + def `then`[B]( + onFulfilled: js.Function1[A, B | Thenable[B]], + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + + new MockPromise[B]( + { (innerResolve: js.Function1[B | Thenable[B], _], + innerReject: js.Function1[scala.Any, _]) => + + def doFulfilled(value: A): Unit = { + tryCatchAny[Unit] { + innerResolve(onFulfilled(value)) + } { e => + innerReject(e) + } + } + + def doRejected(reason: Any): Unit = { + tryCatchAny[Unit] { + onRejected.fold[Unit] { + innerReject(reason) + } { onRejectedFun => + innerResolve(onRejectedFun(reason)) + } + } { e => + innerReject(e) + } + } + + state match { + case Pending => + fulfillReactions += doFulfilled _ + rejectReactions += doRejected _ + + case Fulfilled(value) => + enqueue(() => doFulfilled(value)) + + case Rejected(reason) => + enqueue(() => doRejected(reason)) + } + } + ) + } + + def `then`[B >: A]( + onFulfilled: Unit, + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + `then`((x: A) => (x: B | Thenable[B]), onRejected) + } + + // 25.4.5.1 Promise.prototype.catch + def `catch`[B >: A]( + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + `then`((), onRejected) + } + } +} From 62965b9898aa8e8ee5734136f6b6315d2c570c96 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 16 Mar 2021 20:45:38 +0100 Subject: [PATCH 9/9] Do not suggest implicits from scalajs.js.| Now that we unpickle Scala.js unions as actual unions, these implicits are suddenly applicable in many more situations and can end up polluting error messages with implicit suggestions since they should never be used explicitly, so blacklist them from the suggestions. --- .../src/dotty/tools/dotc/typer/ImportSuggestions.scala | 4 ++++ tests/neg-scalajs/type-mismatch.check | 7 +++++++ tests/neg-scalajs/type-mismatch.scala | 8 ++++++++ 3 files changed, 19 insertions(+) create mode 100644 tests/neg-scalajs/type-mismatch.check create mode 100644 tests/neg-scalajs/type-mismatch.scala diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 20564f56e490..7c11508ece65 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -2,11 +2,13 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import Contexts._, Types._, Symbols._, Names._, Decorators._, ProtoTypes._ import Flags._, SymDenotations._ import NameKinds.FlatName import NameOps._ +import StdNames._ import config.Printers.{implicits, implicitsDetailed} import util.Spans.Span import ast.{untpd, tpd} @@ -64,6 +66,8 @@ trait ImportSuggestions: else !root.name.is(FlatName) && !root.name.lastPart.contains('$') && root.is(ModuleVal, butNot = JavaDefined) + // The implicits in `scalajs.js.|` are implementation details and shouldn't be suggested + && !(root.name == nme.raw.BAR && ctx.settings.scalajs.value && root == JSDefinitions.jsdefn.PseudoUnionModule) } def nestedRoots(site: Type)(using Context): List[Symbol] = diff --git a/tests/neg-scalajs/type-mismatch.check b/tests/neg-scalajs/type-mismatch.check new file mode 100644 index 000000000000..2ea5f1f23945 --- /dev/null +++ b/tests/neg-scalajs/type-mismatch.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-scalajs/type-mismatch.scala:6:17 ---------------------------------------------- +6 | val n: Int = msg // error message shouldn't mention implicits from `scalajs.js.|` + | ^^^ + | Found: (msg : String) + | Required: Int + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-scalajs/type-mismatch.scala b/tests/neg-scalajs/type-mismatch.scala new file mode 100644 index 000000000000..09a10237ff1d --- /dev/null +++ b/tests/neg-scalajs/type-mismatch.scala @@ -0,0 +1,8 @@ +import scala.scalajs.js._ + +object HelloWorld { + def main(args: Array[String]): Unit = { + val msg = "hello" + val n: Int = msg // error message shouldn't mention implicits from `scalajs.js.|` + } +}