Skip to content

Commit 837b1cc

Browse files
authored
Merge pull request #15720 from dotty-staging/sjs-dynamic-import
Fix #15701: Implement js.dynamicImport for dynamic module loading.
2 parents 634c580 + ffd2e97 commit 837b1cc

File tree

11 files changed

+344
-112
lines changed

11 files changed

+344
-112
lines changed

.github/workflows/ci.yaml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,14 @@ jobs:
122122

123123
- name: Cmd Tests
124124
run: |
125-
./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"
125+
./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"
126126
./project/scripts/cmdTests
127127
./project/scripts/bootstrappedOnlyCmdTests
128128
129+
- name: Scala.js Test
130+
run: |
131+
./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test"
132+
129133
test_windows_fast:
130134
runs-on: [self-hosted, Windows]
131135
if: "(
@@ -167,7 +171,7 @@ jobs:
167171
shell: cmd
168172

169173
- name: Scala.js Test
170-
run: sbt ";sjsJUnitTests/test ;sjsCompilerTests/test"
174+
run: sbt ";sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test"
171175
shell: cmd
172176

173177
test_windows_full:
@@ -193,7 +197,7 @@ jobs:
193197
shell: cmd
194198

195199
- name: Scala.js Test
196-
run: sbt ";sjsJUnitTests/test ;sjsCompilerTests/test"
200+
run: sbt ";sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test"
197201
shell: cmd
198202

199203
mima:
@@ -464,10 +468,14 @@ jobs:
464468

465469
- name: Test
466470
run: |
467-
./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"
471+
./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"
468472
./project/scripts/cmdTests
469473
./project/scripts/bootstrappedOnlyCmdTests
470474
475+
- name: Scala.js Test
476+
run: |
477+
./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test"
478+
471479
publish_nightly:
472480
runs-on: [self-hosted, Linux]
473481
container:

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

Lines changed: 101 additions & 12 deletions
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))
@@ -350,14 +353,17 @@ class JSCodeGen()(using genCtx: Context) {
350353
tree match {
351354
case EmptyTree => ()
352355

353-
case _: ValDef =>
354-
() // fields are added via genClassFields()
356+
case vd: ValDef =>
357+
// fields are added via genClassFields(), but we need to generate the JS native members
358+
val sym = vd.symbol
359+
if (!sym.is(Module) && sym.hasAnnotation(jsdefn.JSNativeAnnot))
360+
generatedNonFieldMembers += genJSNativeMemberDef(vd)
355361

356362
case dd: DefDef =>
357363
val sym = dd.symbol
358-
359-
if (sym.hasAnnotation(jsdefn.JSNativeAnnot))
360-
generatedNonFieldMembers += genJSNativeMemberDef(dd)
364+
if sym.hasAnnotation(jsdefn.JSNativeAnnot) then
365+
if !sym.is(Accessor) then
366+
generatedNonFieldMembers += genJSNativeMemberDef(dd)
361367
else
362368
generatedNonFieldMembers ++= genMethod(dd)
363369

@@ -404,8 +410,12 @@ class JSCodeGen()(using genCtx: Context) {
404410
Nil
405411
}
406412

413+
val optDynamicImportForwarder =
414+
if (isDynamicImportThunk) List(genDynamicImportForwarder(sym))
415+
else Nil
416+
407417
val allMemberDefsExceptStaticForwarders =
408-
generatedMembers ::: memberExports ::: optStaticInitializer
418+
generatedMembers ::: memberExports ::: optStaticInitializer ::: optDynamicImportForwarder
409419

410420
// Add static forwarders
411421
val allMemberDefs = if (!isCandidateForForwarders(sym)) {
@@ -1372,12 +1382,12 @@ class JSCodeGen()(using genCtx: Context) {
13721382
// Generate a method -------------------------------------------------------
13731383

13741384
/** Generates the JSNativeMemberDef. */
1375-
def genJSNativeMemberDef(tree: DefDef): js.JSNativeMemberDef = {
1385+
def genJSNativeMemberDef(tree: ValOrDefDef): js.JSNativeMemberDef = {
13761386
implicit val pos = tree.span
13771387

13781388
val sym = tree.symbol
13791389
val flags = js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic)
1380-
val methodName = encodeMethodSym(sym)
1390+
val methodName = encodeJSNativeMemberSym(sym)
13811391
val jsNativeLoadSpec = computeJSNativeLoadSpecOfValDef(sym)
13821392
js.JSNativeMemberDef(flags, methodName, jsNativeLoadSpec)
13831393
}
@@ -1775,6 +1785,8 @@ class JSCodeGen()(using genCtx: Context) {
17751785
genLoadModule(sym)
17761786
} else if (sym.is(JavaStatic)) {
17771787
genLoadStaticField(sym)
1788+
} else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) {
1789+
genJSNativeMemberSelect(tree)
17781790
} else {
17791791
val (field, boxed) = genAssignableField(sym, qualifier)
17801792
if (boxed) unbox(field, atPhase(elimErasedValueTypePhase)(sym.info))
@@ -3023,7 +3035,7 @@ class JSCodeGen()(using genCtx: Context) {
30233035
else
30243036
genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))
30253037
} else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) {
3026-
genJSNativeMemberCall(tree, isStat)
3038+
genJSNativeMemberCall(tree)
30273039
} else {
30283040
genApplyMethodMaybeStatically(genExpr(receiver), sym, genActualArgs(sym, args))
30293041
}
@@ -3154,14 +3166,21 @@ class JSCodeGen()(using genCtx: Context) {
31543166
}
31553167

31563168
/** Gen JS code for a call to a native JS def or val. */
3157-
private def genJSNativeMemberCall(tree: Apply, isStat: Boolean): js.Tree = {
3169+
private def genJSNativeMemberSelect(tree: Tree): js.Tree =
3170+
genJSNativeMemberSelectOrCall(tree, Nil)
3171+
3172+
/** Gen JS code for a call to a native JS def or val. */
3173+
private def genJSNativeMemberCall(tree: Apply): js.Tree =
3174+
genJSNativeMemberSelectOrCall(tree, tree.args)
3175+
3176+
/** Gen JS code for a call to a native JS def or val. */
3177+
private def genJSNativeMemberSelectOrCall(tree: Tree, args: List[Tree]): js.Tree = {
31583178
val sym = tree.symbol
3159-
val Apply(_, args) = tree
31603179

31613180
implicit val pos = tree.span
31623181

31633182
val jsNativeMemberValue =
3164-
js.SelectJSNativeMember(encodeClassName(sym.owner), encodeMethodSym(sym))
3183+
js.SelectJSNativeMember(encodeClassName(sym.owner), encodeJSNativeMemberSym(sym))
31653184

31663185
val boxedResult =
31673186
if (sym.isJSGetter) jsNativeMemberValue
@@ -3497,6 +3516,36 @@ class JSCodeGen()(using genCtx: Context) {
34973516
}
34983517
}
34993518

3519+
/** Generates a static method instantiating and calling this
3520+
* DynamicImportThunk's `apply`:
3521+
*
3522+
* {{{
3523+
* static def dynamicImport$;<params>;Ljava.lang.Object(<params>): any = {
3524+
* new <clsSym>.<init>;<params>:V(<params>).apply;Ljava.lang.Object()
3525+
* }
3526+
* }}}
3527+
*/
3528+
private def genDynamicImportForwarder(clsSym: Symbol)(using Position): js.MethodDef = {
3529+
withNewLocalNameScope {
3530+
val ctor = clsSym.primaryConstructor
3531+
val paramSyms = ctor.paramSymss.flatten
3532+
val paramDefs = paramSyms.map(genParamDef(_))
3533+
3534+
val body = {
3535+
val inst = js.New(encodeClassName(clsSym), encodeMethodSym(ctor), paramDefs.map(_.ref))
3536+
genApplyMethod(inst, jsdefn.DynamicImportThunkClass_apply, Nil)
3537+
}
3538+
3539+
js.MethodDef(
3540+
js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic),
3541+
encodeDynamicImportForwarderIdent(paramSyms),
3542+
NoOriginalName,
3543+
paramDefs,
3544+
jstpe.AnyType,
3545+
Some(body))(OptimizerHints.empty, None)
3546+
}
3547+
}
3548+
35003549
/** Boxes a value of the given type before `elimErasedValueType`.
35013550
*
35023551
* This should be used when sending values to a JavaScript context, which
@@ -3800,6 +3849,46 @@ class JSCodeGen()(using genCtx: Context) {
38003849
// js.import.meta
38013850
js.JSImportMeta()
38023851

3852+
case DYNAMIC_IMPORT =>
3853+
// runtime.dynamicImport
3854+
assert(args.size == 1,
3855+
s"Expected exactly 1 argument for JS primitive $code but got " +
3856+
s"${args.size} at $pos")
3857+
3858+
args.head match {
3859+
case Block(stats, expr @ Typed(Apply(fun @ Select(New(tpt), _), args), _)) =>
3860+
/* stats is always empty if no other compiler plugin is present.
3861+
* However, code instrumentation (notably scoverage) might add
3862+
* statements here. If this is the case, the thunk anonymous class
3863+
* has already been created when the other plugin runs (i.e. the
3864+
* plugin ran after jsinterop).
3865+
*
3866+
* Therefore, it is OK to leave the statements on our side of the
3867+
* dynamic loading boundary.
3868+
*/
3869+
3870+
val clsSym = tpt.symbol
3871+
val ctor = fun.symbol
3872+
3873+
assert(clsSym.isSubClass(jsdefn.DynamicImportThunkClass),
3874+
s"expected subclass of DynamicImportThunk, got: $clsSym at: ${expr.sourcePos}")
3875+
assert(ctor.isPrimaryConstructor,
3876+
s"expected primary constructor, got: $ctor at: ${expr.sourcePos}")
3877+
3878+
js.Block(
3879+
stats.map(genStat(_)),
3880+
js.ApplyDynamicImport(
3881+
js.ApplyFlags.empty,
3882+
encodeClassName(clsSym),
3883+
encodeDynamicImportForwarderIdent(ctor.paramSymss.flatten),
3884+
genActualArgs(ctor, args))
3885+
)
3886+
3887+
case tree =>
3888+
throw new FatalError(
3889+
s"Unexpected argument tree in dynamicImport: $tree/${tree.getClass} at: $pos")
3890+
}
3891+
38033892
case JS_NATIVE =>
38043893
// js.native
38053894
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: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import org.scalajs.ir.UTF8String
2424

2525
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
2626

27+
import JSDefinitions.jsdefn
28+
2729
/** Encoding of symbol names for JavaScript
2830
*
2931
* Some issues that this encoding solves:
@@ -54,6 +56,8 @@ object JSEncoding {
5456
private val ScalaRuntimeNothingClassName = ClassName("scala.runtime.Nothing$")
5557
private val ScalaRuntimeNullClassName = ClassName("scala.runtime.Null$")
5658

59+
private val dynamicImportForwarderSimpleName = SimpleMethodName("dynamicImport$")
60+
5761
// Fresh local name generator ----------------------------------------------
5862

5963
class LocalNameGenerator {
@@ -211,17 +215,35 @@ object JSEncoding {
211215
js.MethodIdent(methodName)
212216
}
213217

214-
def encodeStaticMemberSym(sym: Symbol)(
215-
implicit ctx: Context, pos: ir.Position): js.MethodIdent = {
218+
def encodeJSNativeMemberSym(sym: Symbol)(using Context, ir.Position): js.MethodIdent = {
219+
require(sym.hasAnnotation(jsdefn.JSNativeAnnot),
220+
"encodeJSNativeMemberSym called with non-native symbol: " + sym)
221+
if (sym.is(Method))
222+
encodeMethodSym(sym)
223+
else
224+
encodeFieldSymAsMethod(sym)
225+
}
226+
227+
def encodeStaticMemberSym(sym: Symbol)(using Context, ir.Position): js.MethodIdent = {
216228
require(sym.is(Flags.JavaStaticTerm),
217229
"encodeStaticMemberSym called with non-static symbol: " + sym)
230+
encodeFieldSymAsMethod(sym)
231+
}
218232

233+
private def encodeFieldSymAsMethod(sym: Symbol)(using Context, ir.Position): js.MethodIdent = {
219234
val name = sym.name
220235
val resultTypeRef = paramOrResultTypeRef(sym.info)
221236
val methodName = MethodName(name.mangledString, Nil, resultTypeRef)
222237
js.MethodIdent(methodName)
223238
}
224239

240+
def encodeDynamicImportForwarderIdent(params: List[Symbol])(using Context, ir.Position): js.MethodIdent = {
241+
val paramTypeRefs = params.map(sym => paramOrResultTypeRef(sym.info))
242+
val resultTypeRef = jstpe.ClassRef(ir.Names.ObjectClass)
243+
val methodName = MethodName(dynamicImportForwarderSimpleName, paramTypeRefs, resultTypeRef)
244+
js.MethodIdent(methodName)
245+
}
246+
225247
/** Computes the type ref for a type, to be used in a method signature. */
226248
private def paramOrResultTypeRef(tpe: Type)(using Context): jstpe.TypeRef =
227249
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)

0 commit comments

Comments
 (0)