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/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 ||