Skip to content

Commit 2c260e0

Browse files
committed
Fix #15701: Implement js.dynamicImport for dynamic module loading.
Forward port of the compiler changes in scala-js/scala-js@a640f15
1 parent 634c580 commit 2c260e0

File tree

6 files changed

+157
-15
lines changed

6 files changed

+157
-15
lines changed

compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,13 +324,16 @@ class JSCodeGen()(using genCtx: Context) {
324324

325325
// Optimizer hints
326326

327+
val isDynamicImportThunk = sym.isSubClass(jsdefn.DynamicImportThunkClass)
328+
327329
def isStdLibClassWithAdHocInlineAnnot(sym: Symbol): Boolean = {
328330
val fullName = sym.fullName.toString
329331
(fullName.startsWith("scala.Tuple") && !fullName.endsWith("$")) ||
330332
(fullName.startsWith("scala.collection.mutable.ArrayOps$of"))
331333
}
332334

333335
val shouldMarkInline = (
336+
isDynamicImportThunk ||
334337
sym.hasAnnotation(jsdefn.InlineAnnot) ||
335338
(sym.isAnonymousFunction && !sym.isSubClass(defn.PartialFunctionClass)) ||
336339
isStdLibClassWithAdHocInlineAnnot(sym))
@@ -404,8 +407,12 @@ class JSCodeGen()(using genCtx: Context) {
404407
Nil
405408
}
406409

410+
val optDynamicImportForwarder =
411+
if (isDynamicImportThunk) List(genDynamicImportForwarder(sym))
412+
else Nil
413+
407414
val allMemberDefsExceptStaticForwarders =
408-
generatedMembers ::: memberExports ::: optStaticInitializer
415+
generatedMembers ::: memberExports ::: optStaticInitializer ::: optDynamicImportForwarder
409416

410417
// Add static forwarders
411418
val allMemberDefs = if (!isCandidateForForwarders(sym)) {
@@ -3497,6 +3504,36 @@ class JSCodeGen()(using genCtx: Context) {
34973504
}
34983505
}
34993506

3507+
/** Generates a static method instantiating and calling this
3508+
* DynamicImportThunk's `apply`:
3509+
*
3510+
* {{{
3511+
* static def dynamicImport$;<params>;Ljava.lang.Object(<params>): any = {
3512+
* new <clsSym>.<init>;<params>:V(<params>).apply;Ljava.lang.Object()
3513+
* }
3514+
* }}}
3515+
*/
3516+
private def genDynamicImportForwarder(clsSym: Symbol)(using Position): js.MethodDef = {
3517+
withNewLocalNameScope {
3518+
val ctor = clsSym.primaryConstructor
3519+
val paramSyms = ctor.paramSymss.flatten
3520+
val paramDefs = paramSyms.map(genParamDef(_))
3521+
3522+
val body = {
3523+
val inst = js.New(encodeClassName(clsSym), encodeMethodSym(ctor), paramDefs.map(_.ref))
3524+
genApplyMethod(inst, jsdefn.DynamicImportThunkClass_apply, Nil)
3525+
}
3526+
3527+
js.MethodDef(
3528+
js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic),
3529+
encodeDynamicImportForwarderIdent(paramSyms),
3530+
NoOriginalName,
3531+
paramDefs,
3532+
jstpe.AnyType,
3533+
Some(body))(OptimizerHints.empty, None)
3534+
}
3535+
}
3536+
35003537
/** Boxes a value of the given type before `elimErasedValueType`.
35013538
*
35023539
* This should be used when sending values to a JavaScript context, which
@@ -3800,6 +3837,46 @@ class JSCodeGen()(using genCtx: Context) {
38003837
// js.import.meta
38013838
js.JSImportMeta()
38023839

3840+
case DYNAMIC_IMPORT =>
3841+
// runtime.dynamicImport
3842+
assert(args.size == 1,
3843+
s"Expected exactly 1 argument for JS primitive $code but got " +
3844+
s"${args.size} at $pos")
3845+
3846+
args.head match {
3847+
case Block(stats, expr @ Typed(Apply(fun @ Select(New(tpt), _), args), _)) =>
3848+
/* stats is always empty if no other compiler plugin is present.
3849+
* However, code instrumentation (notably scoverage) might add
3850+
* statements here. If this is the case, the thunk anonymous class
3851+
* has already been created when the other plugin runs (i.e. the
3852+
* plugin ran after jsinterop).
3853+
*
3854+
* Therefore, it is OK to leave the statements on our side of the
3855+
* dynamic loading boundary.
3856+
*/
3857+
3858+
val clsSym = tpt.symbol
3859+
val ctor = fun.symbol
3860+
3861+
assert(clsSym.isSubClass(jsdefn.DynamicImportThunkClass),
3862+
s"expected subclass of DynamicImportThunk, got: $clsSym at: ${expr.sourcePos}")
3863+
assert(ctor.isPrimaryConstructor,
3864+
s"expected primary constructor, got: $ctor at: ${expr.sourcePos}")
3865+
3866+
js.Block(
3867+
stats.map(genStat(_)),
3868+
js.ApplyDynamicImport(
3869+
js.ApplyFlags.empty,
3870+
encodeClassName(clsSym),
3871+
encodeDynamicImportForwarderIdent(ctor.paramSymss.flatten),
3872+
genActualArgs(ctor, args))
3873+
)
3874+
3875+
case tree =>
3876+
throw new FatalError(
3877+
s"Unexpected argument tree in dynamicImport: $tree/${tree.getClass} at: $pos")
3878+
}
3879+
38033880
case JS_NATIVE =>
38043881
// js.native
38053882
report.error(

compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ final class JSDefinitions()(using Context) {
3838
def JSPackage_native(using Context) = JSPackage_nativeR.symbol
3939
@threadUnsafe lazy val JSPackage_undefinedR = ScalaJSJSPackageClass.requiredMethodRef("undefined")
4040
def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol
41+
@threadUnsafe lazy val JSPackage_dynamicImportR = ScalaJSJSPackageClass.requiredMethodRef("dynamicImport")
42+
def JSPackage_dynamicImport(using Context) = JSPackage_dynamicImportR.symbol
4143

4244
@threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native")
4345
def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass
@@ -176,6 +178,13 @@ final class JSDefinitions()(using Context) {
176178
def Runtime_withContextualJSClassValue(using Context) = Runtime_withContextualJSClassValueR.symbol
177179
@threadUnsafe lazy val Runtime_linkingInfoR = RuntimePackageClass.requiredMethodRef("linkingInfo")
178180
def Runtime_linkingInfo(using Context) = Runtime_linkingInfoR.symbol
181+
@threadUnsafe lazy val Runtime_dynamicImportR = RuntimePackageClass.requiredMethodRef("dynamicImport")
182+
def Runtime_dynamicImport(using Context) = Runtime_dynamicImportR.symbol
183+
184+
@threadUnsafe lazy val DynamicImportThunkType: TypeRef = requiredClassRef("scala.scalajs.runtime.DynamicImportThunk")
185+
def DynamicImportThunkClass(using Context) = DynamicImportThunkType.symbol.asClass
186+
@threadUnsafe lazy val DynamicImportThunkClass_applyR = DynamicImportThunkClass.requiredMethodRef(nme.apply)
187+
def DynamicImportThunkClass_apply(using Context) = DynamicImportThunkClass_applyR.symbol
179188

180189
@threadUnsafe lazy val SpecialPackageVal = requiredPackage("scala.scalajs.js.special")
181190
@threadUnsafe lazy val SpecialPackageClass = SpecialPackageVal.moduleClass.asClass

compiler/src/dotty/tools/backend/sjs/JSEncoding.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ object JSEncoding {
5454
private val ScalaRuntimeNothingClassName = ClassName("scala.runtime.Nothing$")
5555
private val ScalaRuntimeNullClassName = ClassName("scala.runtime.Null$")
5656

57+
private val dynamicImportForwarderSimpleName = SimpleMethodName("dynamicImport$")
58+
5759
// Fresh local name generator ----------------------------------------------
5860

5961
class LocalNameGenerator {
@@ -222,6 +224,13 @@ object JSEncoding {
222224
js.MethodIdent(methodName)
223225
}
224226

227+
def encodeDynamicImportForwarderIdent(params: List[Symbol])(using Context, ir.Position): js.MethodIdent = {
228+
val paramTypeRefs = params.map(sym => paramOrResultTypeRef(sym.info))
229+
val resultTypeRef = jstpe.ClassRef(ir.Names.ObjectClass)
230+
val methodName = MethodName(dynamicImportForwarderSimpleName, paramTypeRefs, resultTypeRef)
231+
js.MethodIdent(methodName)
232+
}
233+
225234
/** Computes the type ref for a type, to be used in a method signature. */
226235
private def paramOrResultTypeRef(tpe: Type)(using Context): jstpe.TypeRef =
227236
toParamOrResultTypeRef(toTypeRef(tpe))

compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ object JSPrimitives {
3232
inline val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass
3333
inline val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue
3434
inline val LINKING_INFO = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.linkingInfo
35+
inline val DYNAMIC_IMPORT = LINKING_INFO + 1 // runtime.dynamicImport
3536

36-
inline val STRICT_EQ = LINKING_INFO + 1 // js.special.strictEquals
37-
inline val IN = STRICT_EQ + 1 // js.special.in
38-
inline val INSTANCEOF = IN + 1 // js.special.instanceof
39-
inline val DELETE = INSTANCEOF + 1 // js.special.delete
40-
inline val FORIN = DELETE + 1 // js.special.forin
41-
inline val DEBUGGER = FORIN + 1 // js.special.debugger
37+
inline val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals
38+
inline val IN = STRICT_EQ + 1 // js.special.in
39+
inline val INSTANCEOF = IN + 1 // js.special.instanceof
40+
inline val DELETE = INSTANCEOF + 1 // js.special.delete
41+
inline val FORIN = DELETE + 1 // js.special.forin
42+
inline val DEBUGGER = FORIN + 1 // js.special.debugger
4243

4344
inline val THROW = DEBUGGER + 1
4445

@@ -113,6 +114,7 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) {
113114
addPrimitive(jsdefn.Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS)
114115
addPrimitive(jsdefn.Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE)
115116
addPrimitive(jsdefn.Runtime_linkingInfo, LINKING_INFO)
117+
addPrimitive(jsdefn.Runtime_dynamicImport, DYNAMIC_IMPORT)
116118

117119
addPrimitive(jsdefn.Special_strictEquals, STRICT_EQ)
118120
addPrimitive(jsdefn.Special_in, IN)

compiler/src/dotty/tools/dotc/transform/Dependencies.scala

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import SymUtils.*
77
import collection.mutable.{LinkedHashMap, TreeSet}
88
import annotation.constructorOnly
99

10+
import dotty.tools.backend.sjs.JSDefinitions.jsdefn
11+
1012
/** Exposes the dependencies of the `root` tree in three functions or maps:
1113
* `freeVars`, `tracked`, and `logicalOwner`.
1214
*/
@@ -182,14 +184,24 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co
182184
def setLogicOwner(local: Symbol) =
183185
val encClass = local.owner.enclosingClass
184186
val preferEncClass =
185-
encClass.isStatic
186-
// non-static classes can capture owners, so should be avoided
187-
&& (encClass.isProperlyContainedIn(local.topLevelClass)
188-
// can be false for symbols which are defined in some weird combination of supercalls.
189-
|| encClass.is(ModuleClass, butNot = Package)
190-
// needed to not cause deadlocks in classloader. see t5375.scala
191-
)
192-
logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass
187+
(
188+
encClass.isStatic
189+
// non-static classes can capture owners, so should be avoided
190+
&& (encClass.isProperlyContainedIn(local.topLevelClass)
191+
// can be false for symbols which are defined in some weird combination of supercalls.
192+
|| encClass.is(ModuleClass, butNot = Package)
193+
// needed to not cause deadlocks in classloader. see t5375.scala
194+
)
195+
)
196+
|| (
197+
/* Scala.js: Never move any member beyond the boundary of a DynamicImportThunk.
198+
* DynamicImportThunk subclasses are boundaries between the eventual ES modules
199+
* that can be dynamically loaded. Moving members across that boundary changes
200+
* the dynamic and static dependencies between ES modules, which is forbidden.
201+
*/
202+
ctx.settings.scalajs.value && encClass.isSubClass(jsdefn.DynamicImportThunkClass)
203+
)
204+
logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass
193205

194206
tree match
195207
case tree: Ident =>

compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,39 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
279279
val ctorOf = ref(jsdefn.JSPackage_constructorOf).appliedToTypeTree(tpeArg)
280280
ref(jsdefn.Runtime_newConstructorTag).appliedToType(tpeArg.tpe).appliedTo(ctorOf)
281281

282+
/* Rewrite js.dynamicImport[T](body) into
283+
*
284+
* runtime.dynamicImport[T](
285+
* new DynamicImportThunk { def apply(): Any = body }
286+
* )
287+
*/
288+
case Apply(TypeApply(fun, List(tpeArg)), List(body))
289+
if fun.symbol == jsdefn.JSPackage_dynamicImport =>
290+
val span = tree.span
291+
val currentOwner = ctx.owner
292+
293+
assert(currentOwner.isTerm, s"unexpected owner: $currentOwner at ${tree.sourcePos}")
294+
295+
val cls = newNormalizedClassSymbol(currentOwner, tpnme.ANON_CLASS, Synthetic | Final,
296+
List(jsdefn.DynamicImportThunkType), coord = span)
297+
val constr = newConstructor(cls, Synthetic, Nil, Nil).entered
298+
299+
val applySym = newSymbol(cls, nme.apply, Method, MethodType(Nil, Nil, defn.AnyType), coord = span).entered
300+
val newBody = transform(body).changeOwnerAfter(currentOwner, applySym, thisPhase)
301+
val applyDefDef = DefDef(applySym, newBody)
302+
303+
// class $anon extends DynamicImportThunk
304+
val cdef = ClassDef(cls, DefDef(constr), List(applyDefDef)).withSpan(span)
305+
306+
/* runtime.DynamicImport[A]({
307+
* class $anon ...
308+
* new $anon
309+
* })
310+
*/
311+
ref(jsdefn.Runtime_dynamicImport)
312+
.appliedToTypeTree(tpeArg)
313+
.appliedTo(Block(cdef :: Nil, New(cls.typeRef, Nil)))
314+
282315
// Compile-time errors and warnings for js.Dynamic.literal
283316
case Apply(Apply(fun, nameArgs), args)
284317
if fun.symbol == jsdefn.JSDynamicLiteral_applyDynamic ||

0 commit comments

Comments
 (0)