Skip to content

Commit

Permalink
Fix scala#15701: Implement js.dynamicImport for dynamic module loading.
Browse files Browse the repository at this point in the history
Forward port of the compiler changes in
scala-js/scala-js@a640f15
  • Loading branch information
sjrd committed Jul 21, 2022
1 parent 634c580 commit 3c9c5ba
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 7 deletions.
79 changes: 78 additions & 1 deletion compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,16 @@ 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("$")) ||
(fullName.startsWith("scala.collection.mutable.ArrayOps$of"))
}

val shouldMarkInline = (
isDynamicImportThunk ||
sym.hasAnnotation(jsdefn.InlineAnnot) ||
(sym.isAnonymousFunction && !sym.isSubClass(defn.PartialFunctionClass)) ||
isStdLibClassWithAdHocInlineAnnot(sym))
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -3497,6 +3504,36 @@ class JSCodeGen()(using genCtx: Context) {
}
}

/** Generates a static method instantiating and calling this
* DynamicImportThunk's `apply`:
*
* {{{
* static def dynamicImport$;<params>;Ljava.lang.Object(<params>): any = {
* new <clsSym>.<init>;<params>:V(<params>).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
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/backend/sjs/JSEncoding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down
14 changes: 8 additions & 6 deletions compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down

0 comments on commit 3c9c5ba

Please sign in to comment.