From 2c260e08ea3053885f2b06554c33fb8a13afacdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 21 Jul 2022 15:43:54 +0200 Subject: [PATCH 1/4] Fix #15701: Implement js.dynamicImport for dynamic module loading. Forward port of the compiler changes in https://github.com/scala-js/scala-js/commit/a640f1586cf9543fae29e8202f2bfb02ac246aff --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 79 ++++++++++++++++++- .../tools/backend/sjs/JSDefinitions.scala | 9 +++ .../dotty/tools/backend/sjs/JSEncoding.scala | 9 +++ .../tools/backend/sjs/JSPrimitives.scala | 14 ++-- .../tools/dotc/transform/Dependencies.scala | 28 +++++-- .../dotc/transform/sjs/PrepJSInterop.scala | 33 ++++++++ 6 files changed, 157 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 96608a565adc..94828673ed46 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -324,6 +324,8 @@ class JSCodeGen()(using genCtx: Context) { // Optimizer hints + val isDynamicImportThunk = sym.isSubClass(jsdefn.DynamicImportThunkClass) + def isStdLibClassWithAdHocInlineAnnot(sym: Symbol): Boolean = { val fullName = sym.fullName.toString (fullName.startsWith("scala.Tuple") && !fullName.endsWith("$")) || @@ -331,6 +333,7 @@ class JSCodeGen()(using genCtx: Context) { } val shouldMarkInline = ( + isDynamicImportThunk || sym.hasAnnotation(jsdefn.InlineAnnot) || (sym.isAnonymousFunction && !sym.isSubClass(defn.PartialFunctionClass)) || isStdLibClassWithAdHocInlineAnnot(sym)) @@ -404,8 +407,12 @@ class JSCodeGen()(using genCtx: Context) { Nil } + val optDynamicImportForwarder = + if (isDynamicImportThunk) List(genDynamicImportForwarder(sym)) + else Nil + val allMemberDefsExceptStaticForwarders = - generatedMembers ::: memberExports ::: optStaticInitializer + generatedMembers ::: memberExports ::: optStaticInitializer ::: optDynamicImportForwarder // Add static forwarders val allMemberDefs = if (!isCandidateForForwarders(sym)) { @@ -3497,6 +3504,36 @@ class JSCodeGen()(using genCtx: Context) { } } + /** Generates a static method instantiating and calling this + * DynamicImportThunk's `apply`: + * + * {{{ + * static def dynamicImport$;;Ljava.lang.Object(): any = { + * new .;:V().apply;Ljava.lang.Object() + * } + * }}} + */ + private def genDynamicImportForwarder(clsSym: Symbol)(using Position): js.MethodDef = { + withNewLocalNameScope { + val ctor = clsSym.primaryConstructor + val paramSyms = ctor.paramSymss.flatten + val paramDefs = paramSyms.map(genParamDef(_)) + + val body = { + val inst = js.New(encodeClassName(clsSym), encodeMethodSym(ctor), paramDefs.map(_.ref)) + genApplyMethod(inst, jsdefn.DynamicImportThunkClass_apply, Nil) + } + + js.MethodDef( + js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic), + encodeDynamicImportForwarderIdent(paramSyms), + NoOriginalName, + paramDefs, + jstpe.AnyType, + Some(body))(OptimizerHints.empty, None) + } + } + /** Boxes a value of the given type before `elimErasedValueType`. * * This should be used when sending values to a JavaScript context, which @@ -3800,6 +3837,46 @@ class JSCodeGen()(using genCtx: Context) { // js.import.meta js.JSImportMeta() + case DYNAMIC_IMPORT => + // runtime.dynamicImport + assert(args.size == 1, + s"Expected exactly 1 argument for JS primitive $code but got " + + s"${args.size} at $pos") + + args.head match { + case Block(stats, expr @ Typed(Apply(fun @ Select(New(tpt), _), args), _)) => + /* stats is always empty if no other compiler plugin is present. + * However, code instrumentation (notably scoverage) might add + * statements here. If this is the case, the thunk anonymous class + * has already been created when the other plugin runs (i.e. the + * plugin ran after jsinterop). + * + * Therefore, it is OK to leave the statements on our side of the + * dynamic loading boundary. + */ + + val clsSym = tpt.symbol + val ctor = fun.symbol + + assert(clsSym.isSubClass(jsdefn.DynamicImportThunkClass), + s"expected subclass of DynamicImportThunk, got: $clsSym at: ${expr.sourcePos}") + assert(ctor.isPrimaryConstructor, + s"expected primary constructor, got: $ctor at: ${expr.sourcePos}") + + js.Block( + stats.map(genStat(_)), + js.ApplyDynamicImport( + js.ApplyFlags.empty, + encodeClassName(clsSym), + encodeDynamicImportForwarderIdent(ctor.paramSymss.flatten), + genActualArgs(ctor, args)) + ) + + case tree => + throw new FatalError( + s"Unexpected argument tree in dynamicImport: $tree/${tree.getClass} at: $pos") + } + case JS_NATIVE => // js.native report.error( diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 99dad4deb203..8dd5ba7c507c 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -38,6 +38,8 @@ final class JSDefinitions()(using Context) { def JSPackage_native(using Context) = JSPackage_nativeR.symbol @threadUnsafe lazy val JSPackage_undefinedR = ScalaJSJSPackageClass.requiredMethodRef("undefined") def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol + @threadUnsafe lazy val JSPackage_dynamicImportR = ScalaJSJSPackageClass.requiredMethodRef("dynamicImport") + def JSPackage_dynamicImport(using Context) = JSPackage_dynamicImportR.symbol @threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native") def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass @@ -176,6 +178,13 @@ final class JSDefinitions()(using Context) { def Runtime_withContextualJSClassValue(using Context) = Runtime_withContextualJSClassValueR.symbol @threadUnsafe lazy val Runtime_linkingInfoR = RuntimePackageClass.requiredMethodRef("linkingInfo") def Runtime_linkingInfo(using Context) = Runtime_linkingInfoR.symbol + @threadUnsafe lazy val Runtime_dynamicImportR = RuntimePackageClass.requiredMethodRef("dynamicImport") + def Runtime_dynamicImport(using Context) = Runtime_dynamicImportR.symbol + + @threadUnsafe lazy val DynamicImportThunkType: TypeRef = requiredClassRef("scala.scalajs.runtime.DynamicImportThunk") + def DynamicImportThunkClass(using Context) = DynamicImportThunkType.symbol.asClass + @threadUnsafe lazy val DynamicImportThunkClass_applyR = DynamicImportThunkClass.requiredMethodRef(nme.apply) + def DynamicImportThunkClass_apply(using Context) = DynamicImportThunkClass_applyR.symbol @threadUnsafe lazy val SpecialPackageVal = requiredPackage("scala.scalajs.js.special") @threadUnsafe lazy val SpecialPackageClass = SpecialPackageVal.moduleClass.asClass diff --git a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala index cf89873b9c80..bd1d079bc66e 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala @@ -54,6 +54,8 @@ object JSEncoding { private val ScalaRuntimeNothingClassName = ClassName("scala.runtime.Nothing$") private val ScalaRuntimeNullClassName = ClassName("scala.runtime.Null$") + private val dynamicImportForwarderSimpleName = SimpleMethodName("dynamicImport$") + // Fresh local name generator ---------------------------------------------- class LocalNameGenerator { @@ -222,6 +224,13 @@ object JSEncoding { js.MethodIdent(methodName) } + def encodeDynamicImportForwarderIdent(params: List[Symbol])(using Context, ir.Position): js.MethodIdent = { + val paramTypeRefs = params.map(sym => paramOrResultTypeRef(sym.info)) + val resultTypeRef = jstpe.ClassRef(ir.Names.ObjectClass) + val methodName = MethodName(dynamicImportForwarderSimpleName, paramTypeRefs, resultTypeRef) + js.MethodIdent(methodName) + } + /** Computes the type ref for a type, to be used in a method signature. */ private def paramOrResultTypeRef(tpe: Type)(using Context): jstpe.TypeRef = toParamOrResultTypeRef(toTypeRef(tpe)) diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index 000ec334a127..165965d74f7c 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -32,13 +32,14 @@ object JSPrimitives { inline val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass inline val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue inline val LINKING_INFO = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.linkingInfo + inline val DYNAMIC_IMPORT = LINKING_INFO + 1 // runtime.dynamicImport - inline val STRICT_EQ = LINKING_INFO + 1 // js.special.strictEquals - inline val IN = STRICT_EQ + 1 // js.special.in - inline val INSTANCEOF = IN + 1 // js.special.instanceof - inline val DELETE = INSTANCEOF + 1 // js.special.delete - inline val FORIN = DELETE + 1 // js.special.forin - inline val DEBUGGER = FORIN + 1 // js.special.debugger + inline val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals + inline val IN = STRICT_EQ + 1 // js.special.in + inline val INSTANCEOF = IN + 1 // js.special.instanceof + inline val DELETE = INSTANCEOF + 1 // js.special.delete + inline val FORIN = DELETE + 1 // js.special.forin + inline val DEBUGGER = FORIN + 1 // js.special.debugger inline val THROW = DEBUGGER + 1 @@ -113,6 +114,7 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(jsdefn.Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS) addPrimitive(jsdefn.Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE) addPrimitive(jsdefn.Runtime_linkingInfo, LINKING_INFO) + addPrimitive(jsdefn.Runtime_dynamicImport, DYNAMIC_IMPORT) addPrimitive(jsdefn.Special_strictEquals, STRICT_EQ) addPrimitive(jsdefn.Special_in, IN) diff --git a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala index 5e0b77dc9f45..0043c43073ed 100644 --- a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala +++ b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala @@ -7,6 +7,8 @@ import SymUtils.* import collection.mutable.{LinkedHashMap, TreeSet} import annotation.constructorOnly +import dotty.tools.backend.sjs.JSDefinitions.jsdefn + /** Exposes the dependencies of the `root` tree in three functions or maps: * `freeVars`, `tracked`, and `logicalOwner`. */ @@ -182,14 +184,24 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co def setLogicOwner(local: Symbol) = val encClass = local.owner.enclosingClass val preferEncClass = - encClass.isStatic - // non-static classes can capture owners, so should be avoided - && (encClass.isProperlyContainedIn(local.topLevelClass) - // can be false for symbols which are defined in some weird combination of supercalls. - || encClass.is(ModuleClass, butNot = Package) - // needed to not cause deadlocks in classloader. see t5375.scala - ) - logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass + ( + encClass.isStatic + // non-static classes can capture owners, so should be avoided + && (encClass.isProperlyContainedIn(local.topLevelClass) + // can be false for symbols which are defined in some weird combination of supercalls. + || encClass.is(ModuleClass, butNot = Package) + // needed to not cause deadlocks in classloader. see t5375.scala + ) + ) + || ( + /* Scala.js: Never move any member beyond the boundary of a DynamicImportThunk. + * DynamicImportThunk subclasses are boundaries between the eventual ES modules + * that can be dynamically loaded. Moving members across that boundary changes + * the dynamic and static dependencies between ES modules, which is forbidden. + */ + ctx.settings.scalajs.value && encClass.isSubClass(jsdefn.DynamicImportThunkClass) + ) + logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass tree match case tree: Ident => diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 4f7ae5ac60f1..606763a109c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -279,6 +279,39 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP val ctorOf = ref(jsdefn.JSPackage_constructorOf).appliedToTypeTree(tpeArg) ref(jsdefn.Runtime_newConstructorTag).appliedToType(tpeArg.tpe).appliedTo(ctorOf) + /* Rewrite js.dynamicImport[T](body) into + * + * runtime.dynamicImport[T]( + * new DynamicImportThunk { def apply(): Any = body } + * ) + */ + case Apply(TypeApply(fun, List(tpeArg)), List(body)) + if fun.symbol == jsdefn.JSPackage_dynamicImport => + val span = tree.span + val currentOwner = ctx.owner + + assert(currentOwner.isTerm, s"unexpected owner: $currentOwner at ${tree.sourcePos}") + + val cls = newNormalizedClassSymbol(currentOwner, tpnme.ANON_CLASS, Synthetic | Final, + List(jsdefn.DynamicImportThunkType), coord = span) + val constr = newConstructor(cls, Synthetic, Nil, Nil).entered + + val applySym = newSymbol(cls, nme.apply, Method, MethodType(Nil, Nil, defn.AnyType), coord = span).entered + val newBody = transform(body).changeOwnerAfter(currentOwner, applySym, thisPhase) + val applyDefDef = DefDef(applySym, newBody) + + // class $anon extends DynamicImportThunk + val cdef = ClassDef(cls, DefDef(constr), List(applyDefDef)).withSpan(span) + + /* runtime.DynamicImport[A]({ + * class $anon ... + * new $anon + * }) + */ + ref(jsdefn.Runtime_dynamicImport) + .appliedToTypeTree(tpeArg) + .appliedTo(Block(cdef :: Nil, New(cls.typeRef, Nil))) + // Compile-time errors and warnings for js.Dynamic.literal case Apply(Apply(fun, nameArgs), args) if fun.symbol == jsdefn.JSDynamicLiteral_applyDynamic || From 61519a3712799a801695c7ca06aaa2169fe10967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 21 Jul 2022 18:27:10 +0200 Subject: [PATCH 2/4] Scala.js: Handle `@js.native private val`s. In dotc, `private val`s do not generate a getter method, so always relying on the getter to generate the `JSNativeMemberDef` and reading it is not correct. We now generate `JSNativeMemberDef`s both for `ValDef`s and for `DefDef`s that are not Accessors. For reading, we support both `Select`s of fields and `Apply`s of methods. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 34 +++++++++++++------ .../dotty/tools/backend/sjs/JSEncoding.scala | 17 ++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 94828673ed46..324d51e57130 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -353,14 +353,17 @@ class JSCodeGen()(using genCtx: Context) { tree match { case EmptyTree => () - case _: ValDef => - () // fields are added via genClassFields() + case vd: ValDef => + // fields are added via genClassFields(), but we need to generate the JS native members + val sym = vd.symbol + if (!sym.is(Module) && sym.hasAnnotation(jsdefn.JSNativeAnnot)) + generatedNonFieldMembers += genJSNativeMemberDef(vd) case dd: DefDef => val sym = dd.symbol - - if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) - generatedNonFieldMembers += genJSNativeMemberDef(dd) + if sym.hasAnnotation(jsdefn.JSNativeAnnot) then + if !sym.is(Accessor) then + generatedNonFieldMembers += genJSNativeMemberDef(dd) else generatedNonFieldMembers ++= genMethod(dd) @@ -1379,12 +1382,12 @@ class JSCodeGen()(using genCtx: Context) { // Generate a method ------------------------------------------------------- /** Generates the JSNativeMemberDef. */ - def genJSNativeMemberDef(tree: DefDef): js.JSNativeMemberDef = { + def genJSNativeMemberDef(tree: ValOrDefDef): js.JSNativeMemberDef = { implicit val pos = tree.span val sym = tree.symbol val flags = js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic) - val methodName = encodeMethodSym(sym) + val methodName = encodeJSNativeMemberSym(sym) val jsNativeLoadSpec = computeJSNativeLoadSpecOfValDef(sym) js.JSNativeMemberDef(flags, methodName, jsNativeLoadSpec) } @@ -1782,6 +1785,8 @@ class JSCodeGen()(using genCtx: Context) { genLoadModule(sym) } else if (sym.is(JavaStatic)) { genLoadStaticField(sym) + } else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) { + genJSNativeMemberSelect(tree) } else { val (field, boxed) = genAssignableField(sym, qualifier) if (boxed) unbox(field, atPhase(elimErasedValueTypePhase)(sym.info)) @@ -3030,7 +3035,7 @@ class JSCodeGen()(using genCtx: Context) { else genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args)) } else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) { - genJSNativeMemberCall(tree, isStat) + genJSNativeMemberCall(tree) } else { genApplyMethodMaybeStatically(genExpr(receiver), sym, genActualArgs(sym, args)) } @@ -3161,14 +3166,21 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen JS code for a call to a native JS def or val. */ - private def genJSNativeMemberCall(tree: Apply, isStat: Boolean): js.Tree = { + private def genJSNativeMemberSelect(tree: Tree): js.Tree = + genJSNativeMemberSelectOrCall(tree, Nil) + + /** Gen JS code for a call to a native JS def or val. */ + private def genJSNativeMemberCall(tree: Apply): js.Tree = + genJSNativeMemberSelectOrCall(tree, tree.args) + + /** Gen JS code for a call to a native JS def or val. */ + private def genJSNativeMemberSelectOrCall(tree: Tree, args: List[Tree]): js.Tree = { val sym = tree.symbol - val Apply(_, args) = tree implicit val pos = tree.span val jsNativeMemberValue = - js.SelectJSNativeMember(encodeClassName(sym.owner), encodeMethodSym(sym)) + js.SelectJSNativeMember(encodeClassName(sym.owner), encodeJSNativeMemberSym(sym)) val boxedResult = if (sym.isJSGetter) jsNativeMemberValue diff --git a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala index bd1d079bc66e..73a150c60290 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala @@ -24,6 +24,8 @@ import org.scalajs.ir.UTF8String import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions +import JSDefinitions.jsdefn + /** Encoding of symbol names for JavaScript * * Some issues that this encoding solves: @@ -213,11 +215,22 @@ object JSEncoding { js.MethodIdent(methodName) } - def encodeStaticMemberSym(sym: Symbol)( - implicit ctx: Context, pos: ir.Position): js.MethodIdent = { + def encodeJSNativeMemberSym(sym: Symbol)(using Context, ir.Position): js.MethodIdent = { + require(sym.hasAnnotation(jsdefn.JSNativeAnnot), + "encodeJSNativeMemberSym called with non-native symbol: " + sym) + if (sym.is(Method)) + encodeMethodSym(sym) + else + encodeFieldSymAsMethod(sym) + } + + def encodeStaticMemberSym(sym: Symbol)(using Context, ir.Position): js.MethodIdent = { require(sym.is(Flags.JavaStaticTerm), "encodeStaticMemberSym called with non-static symbol: " + sym) + encodeFieldSymAsMethod(sym) + } + private def encodeFieldSymAsMethod(sym: Symbol)(using Context, ir.Position): js.MethodIdent = { val name = sym.name val resultTypeRef = paramOrResultTypeRef(sym.info) val methodName = MethodName(name.mangledString, Nil, resultTypeRef) From 6449e96ab2270aa620d05eb5c9158cbe26d65823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 21 Jul 2022 18:32:32 +0200 Subject: [PATCH 3/4] Scala.js: Test the Scala.js JUnit test suite in ESModule mode. This ensures that the module-only features are tested. --- .github/workflows/ci.yaml | 16 ++++++++--- project/Build.scala | 59 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dcae935b2ef1..d8206829d915 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -122,10 +122,14 @@ jobs: - name: Cmd Tests run: | - ./project/scripts/sbt ";dist/pack; scala3-bootstrapped/compile; scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test; scala3-compiler-bootstrapped/scala3CompilerCoursierTest:test" + ./project/scripts/sbt ";dist/pack; scala3-bootstrapped/compile; scala3-bootstrapped/test ;sbt-test/scripted scala2-compat/* ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test; scala3-compiler-bootstrapped/scala3CompilerCoursierTest:test" ./project/scripts/cmdTests ./project/scripts/bootstrappedOnlyCmdTests + - name: Scala.js Test + run: | + ./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test" + test_windows_fast: runs-on: [self-hosted, Windows] if: "( @@ -167,7 +171,7 @@ jobs: shell: cmd - name: Scala.js Test - run: sbt ";sjsJUnitTests/test ;sjsCompilerTests/test" + run: sbt ";sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test" shell: cmd test_windows_full: @@ -193,7 +197,7 @@ jobs: shell: cmd - name: Scala.js Test - run: sbt ";sjsJUnitTests/test ;sjsCompilerTests/test" + run: sbt ";sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test" shell: cmd mima: @@ -464,10 +468,14 @@ jobs: - name: Test run: | - ./project/scripts/sbt ";dist/pack ;scala3-bootstrapped/compile ;scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test" + ./project/scripts/sbt ";dist/pack ;scala3-bootstrapped/compile ;scala3-bootstrapped/test ;sbt-test/scripted scala2-compat/* ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test" ./project/scripts/cmdTests ./project/scripts/bootstrappedOnlyCmdTests + - name: Scala.js Test + run: | + ./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test" + publish_nightly: runs-on: [self-hosted, Linux] container: diff --git a/project/Build.scala b/project/Build.scala index 453b34901807..4c87913946b1 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -24,11 +24,19 @@ import sbtbuildinfo.BuildInfoPlugin.autoImport._ import scala.util.Properties.isJavaAtLeast import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ -import org.scalajs.linker.interface.ModuleInitializer +import org.scalajs.linker.interface.{ModuleInitializer, StandardConfig} object DottyJSPlugin extends AutoPlugin { import Build._ + object autoImport { + val switchToESModules: StandardConfig => StandardConfig = + config => config.withModuleKind(ModuleKind.ESModule) + } + + val writePackageJSON = taskKey[Unit]( + "Write package.json to configure module type for Node.js") + override def requires: Plugins = ScalaJSPlugin override def projectSettings: Seq[Setting[_]] = Def.settings( @@ -51,6 +59,21 @@ object DottyJSPlugin extends AutoPlugin { // Typecheck the Scala.js IR found on the classpath scalaJSLinkerConfig ~= (_.withCheckIR(true)), + + Compile / jsEnvInput := (Compile / jsEnvInput).dependsOn(writePackageJSON).value, + Test / jsEnvInput := (Test / jsEnvInput).dependsOn(writePackageJSON).value, + + writePackageJSON := { + val packageType = scalaJSLinkerConfig.value.moduleKind match { + case ModuleKind.NoModule => "commonjs" + case ModuleKind.CommonJSModule => "commonjs" + case ModuleKind.ESModule => "module" + } + + val path = target.value / "package.json" + + IO.write(path, s"""{"type": "$packageType"}\n""") + }, ) } @@ -1202,6 +1225,19 @@ object Build { // A first blacklist of tests for those that do not compile or do not link (Test / managedSources) ++= { val dir = fetchScalaJSSource.value / "test-suite" + + val linkerConfig = scalaJSStage.value match { + case FastOptStage => (Test / fastLinkJS / scalaJSLinkerConfig).value + case FullOptStage => (Test / fullLinkJS / scalaJSLinkerConfig).value + } + + val moduleKind = linkerConfig.moduleKind + val hasModules = moduleKind != ModuleKind.NoModule + + def conditionally(cond: Boolean, subdir: String): Seq[File] = + if (!cond) Nil + else (dir / subdir ** "*.scala").get + ( (dir / "shared/src/test/scala" ** (("*.scala": FileFilter) -- "ReflectiveCallTest.scala" // uses many forms of structural calls that are not allowed in Scala 3 anymore @@ -1220,9 +1256,28 @@ object Build { ++ (dir / "js/src/test/require-2.12" ** "*.scala").get ++ (dir / "js/src/test/require-sam" ** "*.scala").get ++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get - ++ (dir / "js/src/test/require-no-modules" ** "*.scala").get + + ++ conditionally(!hasModules, "js/src/test/require-no-modules") + ++ conditionally(hasModules, "js/src/test/require-modules") + ++ conditionally(hasModules && !linkerConfig.closureCompiler, "js/src/test/require-multi-modules") + ++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-dynamic-import") + ++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-esmodule") ) }, + + Test / managedResources ++= { + val testDir = fetchScalaJSSource.value / "test-suite/js/src/test" + + val common = (testDir / "resources" ** "*.js").get + + val moduleSpecific = scalaJSLinkerConfig.value.moduleKind match { + case ModuleKind.NoModule => Nil + case ModuleKind.CommonJSModule => (testDir / "resources-commonjs" ** "*.js").get + case ModuleKind.ESModule => (testDir / "resources-esmodule" ** "*.js").get + } + + common ++ moduleSpecific + }, ) lazy val sjsCompilerTests = project.in(file("sjs-compiler-tests")). From ffd2e972cfeb56ec5854f3e02b99a2735dfe6396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 23 Jul 2022 12:48:46 +0200 Subject: [PATCH 4/4] Refactoring: Factor out the creation of typed anonymous classes. There are several places where we create typed anonymous classes. All followed the same pattern, which we now factored out in a new `tpe.AnonClass` overload. --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 34 ++++--- .../dotty/tools/dotc/inlines/Inlines.scala | 39 ++++---- .../tools/dotc/transform/ExpandSAMs.scala | 92 +++++++++---------- .../dotc/transform/sjs/PrepJSInterop.scala | 25 ++--- 4 files changed, 96 insertions(+), 94 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 16604c9e83b1..53ad330eea35 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -340,27 +340,35 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { * Its position is the union of all functions in `fns`. */ def AnonClass(parents: List[Type], fns: List[TermSymbol], methNames: List[TermName])(using Context): Block = { - val owner = fns.head.owner + AnonClass(fns.head.owner, parents, fns.map(_.span).reduceLeft(_ union _)) { cls => + def forwarder(fn: TermSymbol, name: TermName) = { + val fwdMeth = fn.copy(cls, name, Synthetic | Method | Final).entered.asTerm + for overridden <- fwdMeth.allOverriddenSymbols do + if overridden.is(Extension) then fwdMeth.setFlag(Extension) + if !overridden.is(Deferred) then fwdMeth.setFlag(Override) + DefDef(fwdMeth, ref(fn).appliedToArgss(_)) + } + fns.lazyZip(methNames).map(forwarder) + } + } + + /** An anonymous class + * + * new parents { body } + * + * with the specified owner and position. + */ + def AnonClass(owner: Symbol, parents: List[Type], coord: Coord)(body: ClassSymbol => List[Tree])(using Context): Block = val parents1 = if (parents.head.classSymbol.is(Trait)) { val head = parents.head.parents.head if (head.isRef(defn.AnyClass)) defn.AnyRefType :: parents else head :: parents } else parents - val cls = newNormalizedClassSymbol(owner, tpnme.ANON_CLASS, Synthetic | Final, parents1, - coord = fns.map(_.span).reduceLeft(_ union _)) + val cls = newNormalizedClassSymbol(owner, tpnme.ANON_CLASS, Synthetic | Final, parents1, coord = coord) val constr = newConstructor(cls, Synthetic, Nil, Nil).entered - def forwarder(fn: TermSymbol, name: TermName) = { - val fwdMeth = fn.copy(cls, name, Synthetic | Method | Final).entered.asTerm - for overridden <- fwdMeth.allOverriddenSymbols do - if overridden.is(Extension) then fwdMeth.setFlag(Extension) - if !overridden.is(Deferred) then fwdMeth.setFlag(Override) - DefDef(fwdMeth, ref(fn).appliedToArgss(_)) - } - val forwarders = fns.lazyZip(methNames).map(forwarder) - val cdef = ClassDef(cls, DefDef(constr), forwarders) + val cdef = ClassDef(cls, DefDef(constr), body(cls)) Block(cdef :: Nil, New(cls.typeRef, Nil)) - } def Import(expr: Tree, selectors: List[untpd.ImportSelector])(using Context): Import = ta.assignType(untpd.Import(expr, selectors), newImportSymbol(ctx.owner, expr)) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 5736bb3b0c2e..d1a88406fe45 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -195,23 +195,26 @@ object Inlines: report.error("inline unapply methods with given parameters before the scrutinee are not supported", fun) val sym = unapp.symbol - val cls = newNormalizedClassSymbol(ctx.owner, tpnme.ANON_CLASS, Synthetic | Final, List(defn.ObjectType), coord = sym.coord) - val constr = newConstructor(cls, Synthetic, Nil, Nil, coord = sym.coord).entered - - val targs = fun match - case TypeApply(_, targs) => targs - case _ => Nil - val unapplyInfo = sym.info match - case info: PolyType => info.instantiate(targs.map(_.tpe)) - case info => info - - val unappplySym = newSymbol(cls, sym.name.toTermName, Synthetic | Method, unapplyInfo, coord = sym.coord).entered - val unapply = DefDef(unappplySym, argss => - inlineCall(fun.appliedToArgss(argss).withSpan(unapp.span))(using ctx.withOwner(unappplySym)) - ) - val cdef = ClassDef(cls, DefDef(constr), List(unapply)) - val newUnapply = Block(cdef :: Nil, New(cls.typeRef, Nil)) - val newFun = newUnapply.select(unappplySym).withSpan(unapp.span) + + var unapplySym1: Symbol = NoSymbol // created from within AnonClass() and used afterwards + + val newUnapply = AnonClass(ctx.owner, List(defn.ObjectType), sym.coord) { cls => + val targs = fun match + case TypeApply(_, targs) => targs + case _ => Nil + val unapplyInfo = sym.info match + case info: PolyType => info.instantiate(targs.map(_.tpe)) + case info => info + + val unapplySym = newSymbol(cls, sym.name.toTermName, Synthetic | Method, unapplyInfo, coord = sym.coord).entered + val unapply = DefDef(unapplySym.asTerm, argss => + inlineCall(fun.appliedToArgss(argss).withSpan(unapp.span))(using ctx.withOwner(unapplySym)) + ) + unapplySym1 = unapplySym + List(unapply) + } + + val newFun = newUnapply.select(unapplySym1).withSpan(unapp.span) cpy.UnApply(unapp)(newFun, trailingImplicits, patterns) end inlinedUnapply @@ -463,4 +466,4 @@ object Inlines: // the opaque type itself. An example is in pos/opaque-inline1.scala. end expand end InlineCall -end Inlines \ No newline at end of file +end Inlines diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index ac29275b2210..63bac2fb0f2a 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -124,55 +124,54 @@ class ExpandSAMs extends MiniPhase: val parents = List( defn.AbstractPartialFunctionClass.typeRef.appliedTo(anonTpe.firstParamTypes.head, anonTpe.resultType), defn.SerializableType) - val pfSym = newNormalizedClassSymbol(anonSym.owner, tpnme.ANON_CLASS, Synthetic | Final, parents, coord = tree.span) - - def overrideSym(sym: Symbol) = sym.copy( - owner = pfSym, - flags = Synthetic | Method | Final | Override, - info = tpe.memberInfo(sym), - coord = tree.span).asTerm.entered - val isDefinedAtFn = overrideSym(defn.PartialFunction_isDefinedAt) - val applyOrElseFn = overrideSym(defn.PartialFunction_applyOrElse) - - def translateMatch(tree: Match, pfParam: Symbol, cases: List[CaseDef], defaultValue: Tree)(using Context) = { - val selector = tree.selector - val selectorTpe = selector.tpe.widen - val defaultSym = newSymbol(pfParam.owner, nme.WILDCARD, SyntheticCase, selectorTpe) - val defaultCase = - CaseDef( - Bind(defaultSym, Underscore(selectorTpe)), - EmptyTree, - defaultValue) - val unchecked = selector.annotated(New(ref(defn.UncheckedAnnot.typeRef))) - cpy.Match(tree)(unchecked, cases :+ defaultCase) - .subst(param.symbol :: Nil, pfParam :: Nil) - // Needed because a partial function can be written as: - // param => param match { case "foo" if foo(param) => param } - // And we need to update all references to 'param' - } - def isDefinedAtRhs(paramRefss: List[List[Tree]])(using Context) = { - val tru = Literal(Constant(true)) - def translateCase(cdef: CaseDef) = - cpy.CaseDef(cdef)(body = tru).changeOwner(anonSym, isDefinedAtFn) - val paramRef = paramRefss.head.head - val defaultValue = Literal(Constant(false)) - translateMatch(pfRHS, paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue) - } + AnonClass(anonSym.owner, parents, tree.span) { pfSym => + def overrideSym(sym: Symbol) = sym.copy( + owner = pfSym, + flags = Synthetic | Method | Final | Override, + info = tpe.memberInfo(sym), + coord = tree.span).asTerm.entered + val isDefinedAtFn = overrideSym(defn.PartialFunction_isDefinedAt) + val applyOrElseFn = overrideSym(defn.PartialFunction_applyOrElse) + + def translateMatch(tree: Match, pfParam: Symbol, cases: List[CaseDef], defaultValue: Tree)(using Context) = { + val selector = tree.selector + val selectorTpe = selector.tpe.widen + val defaultSym = newSymbol(pfParam.owner, nme.WILDCARD, SyntheticCase, selectorTpe) + val defaultCase = + CaseDef( + Bind(defaultSym, Underscore(selectorTpe)), + EmptyTree, + defaultValue) + val unchecked = selector.annotated(New(ref(defn.UncheckedAnnot.typeRef))) + cpy.Match(tree)(unchecked, cases :+ defaultCase) + .subst(param.symbol :: Nil, pfParam :: Nil) + // Needed because a partial function can be written as: + // param => param match { case "foo" if foo(param) => param } + // And we need to update all references to 'param' + } - def applyOrElseRhs(paramRefss: List[List[Tree]])(using Context) = { - val List(paramRef, defaultRef) = paramRefss(1) - def translateCase(cdef: CaseDef) = - cdef.changeOwner(anonSym, applyOrElseFn) - val defaultValue = defaultRef.select(nme.apply).appliedTo(paramRef) - translateMatch(pfRHS, paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue) - } + def isDefinedAtRhs(paramRefss: List[List[Tree]])(using Context) = { + val tru = Literal(Constant(true)) + def translateCase(cdef: CaseDef) = + cpy.CaseDef(cdef)(body = tru).changeOwner(anonSym, isDefinedAtFn) + val paramRef = paramRefss.head.head + val defaultValue = Literal(Constant(false)) + translateMatch(pfRHS, paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue) + } - val constr = newConstructor(pfSym, Synthetic, Nil, Nil).entered - val isDefinedAtDef = transformFollowingDeep(DefDef(isDefinedAtFn, isDefinedAtRhs(_)(using ctx.withOwner(isDefinedAtFn)))) - val applyOrElseDef = transformFollowingDeep(DefDef(applyOrElseFn, applyOrElseRhs(_)(using ctx.withOwner(applyOrElseFn)))) - val pfDef = ClassDef(pfSym, DefDef(constr), List(isDefinedAtDef, applyOrElseDef)) - cpy.Block(tree)(pfDef :: Nil, New(pfSym.typeRef, Nil)) + def applyOrElseRhs(paramRefss: List[List[Tree]])(using Context) = { + val List(paramRef, defaultRef) = paramRefss(1) + def translateCase(cdef: CaseDef) = + cdef.changeOwner(anonSym, applyOrElseFn) + val defaultValue = defaultRef.select(nme.apply).appliedTo(paramRef) + translateMatch(pfRHS, paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue) + } + + val isDefinedAtDef = transformFollowingDeep(DefDef(isDefinedAtFn, isDefinedAtRhs(_)(using ctx.withOwner(isDefinedAtFn)))) + val applyOrElseDef = transformFollowingDeep(DefDef(applyOrElseFn, applyOrElseRhs(_)(using ctx.withOwner(applyOrElseFn)))) + List(isDefinedAtDef, applyOrElseDef) + } } private def checkRefinements(tpe: Type, tree: Tree)(using Context): Type = tpe.dealias match { @@ -184,4 +183,3 @@ class ExpandSAMs extends MiniPhase: tpe } end ExpandSAMs - diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 606763a109c4..c846e3394a2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -292,25 +292,18 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP assert(currentOwner.isTerm, s"unexpected owner: $currentOwner at ${tree.sourcePos}") - val cls = newNormalizedClassSymbol(currentOwner, tpnme.ANON_CLASS, Synthetic | Final, - List(jsdefn.DynamicImportThunkType), coord = span) - val constr = newConstructor(cls, Synthetic, Nil, Nil).entered - - val applySym = newSymbol(cls, nme.apply, Method, MethodType(Nil, Nil, defn.AnyType), coord = span).entered - val newBody = transform(body).changeOwnerAfter(currentOwner, applySym, thisPhase) - val applyDefDef = DefDef(applySym, newBody) - - // class $anon extends DynamicImportThunk - val cdef = ClassDef(cls, DefDef(constr), List(applyDefDef)).withSpan(span) + // new DynamicImportThunk { def apply(): Any = body } + val dynamicImportThunkAnonClass = AnonClass(currentOwner, List(jsdefn.DynamicImportThunkType), span) { cls => + val applySym = newSymbol(cls, nme.apply, Method, MethodType(Nil, Nil, defn.AnyType), coord = span).entered + val newBody = transform(body).changeOwnerAfter(currentOwner, applySym, thisPhase) + val applyDefDef = DefDef(applySym, newBody) + List(applyDefDef) + } - /* runtime.DynamicImport[A]({ - * class $anon ... - * new $anon - * }) - */ + // runtime.DynamicImport[A](new ...) ref(jsdefn.Runtime_dynamicImport) .appliedToTypeTree(tpeArg) - .appliedTo(Block(cdef :: Nil, New(cls.typeRef, Nil))) + .appliedTo(dynamicImportThunkAnonClass) // Compile-time errors and warnings for js.Dynamic.literal case Apply(Apply(fun, nameArgs), args)