Skip to content

Commit

Permalink
Merge pull request #15720 from dotty-staging/sjs-dynamic-import
Browse files Browse the repository at this point in the history
Fix #15701: Implement js.dynamicImport for dynamic module loading.
  • Loading branch information
sjrd authored Jul 23, 2022
2 parents 634c580 + ffd2e97 commit 837b1cc
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 112 deletions.
16 changes: 12 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
113 changes: 101 additions & 12 deletions 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 All @@ -350,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)

Expand Down Expand Up @@ -404,8 +410,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 @@ -1372,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)
}
Expand Down Expand Up @@ -1775,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))
Expand Down Expand Up @@ -3023,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))
}
Expand Down Expand Up @@ -3154,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
Expand Down Expand Up @@ -3497,6 +3516,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 +3849,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
26 changes: 24 additions & 2 deletions compiler/src/dotty/tools/backend/sjs/JSEncoding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -54,6 +56,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 @@ -211,17 +215,35 @@ 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)
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
Loading

0 comments on commit 837b1cc

Please sign in to comment.