Skip to content

Commit

Permalink
Fix scala-js#4201: Dynamic module loading
Browse files Browse the repository at this point in the history
  • Loading branch information
gzm0 committed Oct 22, 2020
1 parent 6d1b77c commit 2929537
Show file tree
Hide file tree
Showing 34 changed files with 605 additions and 43 deletions.
56 changes: 55 additions & 1 deletion compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala
Expand Up @@ -460,13 +460,16 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)

// Optimizer hints

val isDynamicImportThunk = sym.isSubClass(DynamicImportThunkClass)

def isStdLibClassWithAdHocInlineAnnot(sym: Symbol): Boolean = {
val fullName = sym.fullName
(fullName.startsWith("scala.Tuple") && !fullName.endsWith("$")) ||
(fullName.startsWith("scala.collection.mutable.ArrayOps$of"))
}

val shouldMarkInline = (
isDynamicImportThunk ||
sym.hasAnnotation(InlineAnnotationClass) ||
(sym.isAnonymousFunction && !sym.isSubClass(PartialFunctionClass)) ||
isStdLibClassWithAdHocInlineAnnot(sym))
Expand Down Expand Up @@ -544,8 +547,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
}
}

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 @@ -4996,6 +5003,31 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
val arg = genArgs1
js.JSImportCall(arg)

case DYNAMIC_IMPORT =>
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(Nil, Apply(fun @ Select(New(tpt), _), args)) =>
val clsSym = tpt.symbol
val ctor = fun.symbol

assert(clsSym.isSubClass(DynamicImportThunkClass),
s"expected subclass of DynamicImportThunk, got: $clsSym")
assert(ctor.isPrimaryConstructor,
s"expected primary constructor, got: $ctor")

js.ApplyDynamicImport(js.ApplyFlags.empty,
encodeClassName(clsSym),
encodeDynamicImportForwarderIdent(ctor.tpe.params),
genActualArgs(ctor, args))

case tree =>
abort("Unexpected argument tree in dynamicImport: " +
tree + "/" + tree.getClass + " at: " + tree.pos)
}

case STRICT_EQ =>
// js.special.strictEquals(arg1, arg2)
val (arg1, arg2) = genArgs2
Expand Down Expand Up @@ -6167,6 +6199,28 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
(patchedParams, patchedBody)
}

private def genDynamicImportForwarder(clsSym: Symbol)(
implicit pos: Position): js.MethodDef = {
withNewLocalNameScope {
val ctor = clsSym.primaryConstructor
val paramSyms = ctor.tpe.params
val paramDefs = paramSyms.map(genParamDef(_))

val body = {
val inst = genNew(clsSym, ctor, paramDefs.map(_.ref))
genApplyMethod(inst, DynamicImportThunkClass_apply, Nil)
}

js.MethodDef(
js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic),
encodeDynamicImportForwarderIdent(paramSyms),
NoOriginalName,
paramDefs,
jstpe.AnyType,
Some(body))(OptimizerHints.empty, None)
}
}

// Methods to deal with JSName ---------------------------------------------

def genExpr(name: JSName)(implicit pos: Position): js.Tree = name match {
Expand Down
Expand Up @@ -44,6 +44,7 @@ trait JSDefinitions {
lazy val JSPackage_constructorOf = getMemberMethod(ScalaJSJSPackageModule, newTermName("constructorOf"))
lazy val JSPackage_native = getMemberMethod(ScalaJSJSPackageModule, newTermName("native"))
lazy val JSPackage_undefined = getMemberMethod(ScalaJSJSPackageModule, newTermName("undefined"))
lazy val JSPackage_dynamicImport = getMemberMethod(ScalaJSJSPackageModule, newTermName("dynamicImport"))

lazy val JSNativeAnnotation = getRequiredClass("scala.scalajs.js.native")

Expand Down Expand Up @@ -119,6 +120,10 @@ trait JSDefinitions {
lazy val Runtime_privateFieldsSymbol = getMemberMethod(RuntimePackageModule, newTermName("privateFieldsSymbol"))
lazy val Runtime_linkingInfo = getMemberMethod(RuntimePackageModule, newTermName("linkingInfo"))
lazy val Runtime_identityHashCode = getMemberMethod(RuntimePackageModule, newTermName("identityHashCode"))
lazy val Runtime_dynamicImport = getMemberMethod(RuntimePackageModule, newTermName("dynamicImport"))

lazy val DynamicImportThunkClass = getRequiredClass("scala.scalajs.runtime.DynamicImportThunk")
lazy val DynamicImportThunkClass_apply = getMemberMethod(DynamicImportThunkClass, nme.apply)

lazy val Tuple2_apply = getMemberMethod(TupleClass(2).companionModule, nme.apply)

Expand Down
12 changes: 12 additions & 0 deletions compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala
Expand Up @@ -54,6 +54,8 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent {
private val ScalaRuntimeNullClass = ClassName("scala.runtime.Null$")
private val ScalaRuntimeNothingClass = ClassName("scala.runtime.Nothing$")

private val dynamicImportForwarderSimpleName = SimpleMethodName("dynamicImport$")

// Fresh local name generator ----------------------------------------------

private val usedLocalNames = new ScopedVar[mutable.Set[LocalName]]
Expand Down Expand Up @@ -234,6 +236,16 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent {
js.MethodIdent(methodName)
}

def encodeDynamicImportForwarderIdent(params: List[Symbol])(
implicit pos: Position): js.MethodIdent = {
val paramTypeRefs = params.map(sym => paramOrResultTypeRef(sym.tpe))
val resultTypeRef = toTypeRef(definitions.ObjectClass.tpe)
val methodName =
MethodName(dynamicImportForwarderSimpleName, paramTypeRefs, resultTypeRef)

js.MethodIdent(methodName)
}

/** Computes the internal name for a type. */
private def paramOrResultTypeRef(tpe: Type): jstpe.TypeRef = {
toTypeRef(tpe) match {
Expand Down
Expand Up @@ -54,8 +54,9 @@ abstract class JSPrimitives {
final val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue
final val LINKING_INFO = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.linkingInfo
final val IDENTITY_HASH_CODE = LINKING_INFO + 1 // runtime.identityHashCode
final val DYNAMIC_IMPORT = IDENTITY_HASH_CODE + 1 // runtime.dynamicImport

final val STRICT_EQ = IDENTITY_HASH_CODE + 1 // js.special.strictEquals
final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals
final val IN = STRICT_EQ + 1 // js.special.in
final val INSTANCEOF = IN + 1 // js.special.instanceof
final val DELETE = INSTANCEOF + 1 // js.special.delete
Expand Down Expand Up @@ -100,6 +101,7 @@ abstract class JSPrimitives {
WITH_CONTEXTUAL_JS_CLASS_VALUE)
addPrimitive(Runtime_linkingInfo, LINKING_INFO)
addPrimitive(Runtime_identityHashCode, IDENTITY_HASH_CODE)
addPrimitive(Runtime_dynamicImport, DYNAMIC_IMPORT)

addPrimitive(Special_strictEquals, STRICT_EQ)
addPrimitive(Special_in, IN)
Expand Down
43 changes: 43 additions & 0 deletions compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala
Expand Up @@ -375,6 +375,49 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
}
}

/* Rewrite js.dynamicImport[T](body) into
*
* runtime.dynamicImport[A](
* new DynamicImportThunk { def apply(): Any = body }
* )
*/
case Apply(TypeApply(fun, List(tpeArg)), List(body))
if fun.symbol == JSPackage_dynamicImport =>
val pos = tree.pos

assert(currentOwner.isTerm, s"unexpected owner: $currentOwner")

val clsSym = currentOwner.newClass(tpnme.ANON_CLASS_NAME, pos)
clsSym.setInfo( // do not enter the symbol, owner is a term.
ClassInfoType(List(DynamicImportThunkClass.tpe), newScope, clsSym))

val ctorSym = clsSym.newClassConstructor(pos)
ctorSym.setInfoAndEnter(MethodType(Nil, clsSym.tpe))

val applySym = clsSym.newMethod(nme.apply)
applySym.setInfoAndEnter(MethodType(Nil, ObjectTpe))

typer.typed {
atPos(tree.pos) {
// class $anon extends DynamicImportThunk
val clsDef = ClassDef(clsSym, List(
// def <init>(): = super.<init>()
DefDef(ctorSym, Block(gen.mkMethodCall(
Super(clsSym, tpnme.EMPTY), ObjectClass.primaryConstructor, Nil, Nil))),
// def apply(): Any = body
DefDef(applySym, body.setType(ObjectTpe))))

/* runtime.DynamicImport[A]({
* class $anon ...
* new $anon
* })
*/
gen.mkMethodCall(Runtime_dynamicImport,
List(tpeArg.tpe),
List(Block(clsDef, New(clsSym))))
}
}

/* Catch calls to Predef.classOf[T]. These should NEVER reach this phase
* but unfortunately do. In normal cases, the typer phase replaces these
* calls by a literal constant of the given type. However, when we compile
Expand Down
7 changes: 7 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Hashers.scala
Expand Up @@ -272,6 +272,13 @@ object Hashers {
mixTrees(args)
mixType(tree.tpe)

case ApplyDynamicImport(flags, className, method, args) =>
mixTag(TagApplyDynamicImport)
mixInt(ApplyFlags.toBits(flags))
mixName(className)
mixMethodIdent(method)
mixTrees(args)

case UnaryOp(op, lhs) =>
mixTag(TagUnaryOp)
mixInt(op)
Expand Down
8 changes: 8 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Printers.scala
Expand Up @@ -335,6 +335,14 @@ object Printers {
print(method)
printArgs(args)

case ApplyDynamicImport(flags, className, method, args) =>
print("dynamicImport ")
print(className)
print("::")
print(flags)
print(method)
printArgs(args)

case UnaryOp(op, lhs) =>
import UnaryOp._
print('(')
Expand Down
7 changes: 7 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Serializers.scala
Expand Up @@ -354,6 +354,10 @@ object Serializers {
writeApplyFlags(flags); writeName(className); writeMethodIdent(method); writeTrees(args)
writeType(tree.tpe)

case ApplyDynamicImport(flags, className, method, args) =>
writeTagAndPos(TagApplyDynamicImport)
writeApplyFlags(flags); writeName(className); writeMethodIdent(method); writeTrees(args)

case UnaryOp(op, lhs) =>
writeTagAndPos(TagUnaryOp)
writeByte(op); writeTree(lhs)
Expand Down Expand Up @@ -1073,6 +1077,9 @@ object Serializers {
case TagApplyStatic =>
ApplyStatic(readApplyFlags(), readClassName(), readMethodIdent(),
readTrees())(readType())
case TagApplyDynamicImport =>
ApplyDynamicImport(readApplyFlags(), readClassName(),
readMethodIdent(), readTrees())

case TagUnaryOp => UnaryOp(readByte(), readTree())
case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree())
Expand Down
4 changes: 4 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Tags.scala
Expand Up @@ -106,6 +106,10 @@ private[ir] object Tags {
final val TagIdentityHashCode = TagCreateJSClass + 1
final val TagSelectJSNativeMember = TagIdentityHashCode + 1

// New in 1.4

final val TagApplyDynamicImport = TagSelectJSNativeMember + 1

// Tags for member defs

final val TagFieldDef = 1
Expand Down
3 changes: 3 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Transformers.scala
Expand Up @@ -107,6 +107,9 @@ object Transformers {
case ApplyStatic(flags, className, method, args) =>
ApplyStatic(flags, className, method, args map transformExpr)(tree.tpe)

case ApplyDynamicImport(flags, className, method, args) =>
ApplyDynamicImport(flags, className, method, args.map(transformExpr))

case UnaryOp(op, lhs) =>
UnaryOp(op, transformExpr(lhs))

Expand Down
3 changes: 3 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Traversers.scala
Expand Up @@ -98,6 +98,9 @@ object Traversers {
case ApplyStatic(_, _, _, args) =>
args foreach traverse

case ApplyDynamicImport(_, _, _, args) =>
args.foreach(traverse)

case UnaryOp(op, lhs) =>
traverse(lhs)

Expand Down
10 changes: 10 additions & 0 deletions ir/src/main/scala/org/scalajs/ir/Trees.scala
Expand Up @@ -270,6 +270,16 @@ object Trees {
method: MethodIdent, args: List[Tree])(
val tpe: Type)(implicit val pos: Position) extends Tree

/** Apply a static method via dynamic import. */
sealed case class ApplyDynamicImport(flags: ApplyFlags, className: ClassName,
method: MethodIdent, args: List[Tree])(
implicit val pos: Position) extends Tree {
val tpe = AnyType

require(!flags.isPrivate, "invalid flag Private for ApplyDynamicImport")
require(!flags.isConstructor, "invalid flag Constructor for ApplyDynamicImport")
}

/** Unary operation (always preserves pureness). */
sealed case class UnaryOp(op: UnaryOp.Code, lhs: Tree)(
implicit val pos: Position) extends Tree {
Expand Down
5 changes: 5 additions & 0 deletions ir/src/test/scala/org/scalajs/ir/PrintersTest.scala
Expand Up @@ -425,6 +425,11 @@ class PrintersTest {
Nil)(NoType))
}

@Test def printApplyDynamicImportStatic(): Unit = {
assertPrintEquals("dynamicImport test.Test::m;V()",
ApplyDynamicImport(EAF, "test.Test", MethodName("m", Nil, V), Nil))
}

@Test def printUnaryOp(): Unit = {
import UnaryOp._

Expand Down
17 changes: 17 additions & 0 deletions library/src/main/scala/scala/scalajs/js/package.scala
Expand Up @@ -141,4 +141,21 @@ package object js {
"version of the libraries.")
}

/** <span class="badge badge-ecma6" style="float: right;">ECMAScript 6</span>
* Dynamic import boundary for progressive module loading.
*
* {{{
* val promise = js.dynamicImport {
* calculationNeedingLotsOfCode()
* }
*
* promise.foreach(println(_))
* }}}
*
* In the example above, the code required for
* `calculationNeedingLotsOfCode()` is only loaded if the statement is
* actually executed.
*/
def dynamicImport[A](body: => A): js.Promise[A] =
throw new java.lang.Error("stub")
}
@@ -0,0 +1,17 @@
/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.scalajs.runtime

abstract class DynamicImportThunk {
def apply(): Any
}
2 changes: 2 additions & 0 deletions library/src/main/scala/scala/scalajs/runtime/package.scala
Expand Up @@ -104,4 +104,6 @@ package object runtime {
/** Identity hash code of an object. */
def identityHashCode(x: Object): Int = throw new Error("stub")

def dynamicImport[A](thunk: DynamicImportThunk): js.Promise[A] =
throw new Error("stub")
}
Expand Up @@ -70,6 +70,7 @@ object Analysis {

def staticDependencies: scala.collection.Set[ClassName]
def externalDependencies: scala.collection.Set[String]
def dynamicDependencies: scala.collection.Set[ClassName]

def linkedFrom: scala.collection.Seq[From]
def instantiatedFrom: scala.collection.Seq[From]
Expand Down Expand Up @@ -199,6 +200,8 @@ object Analysis {
def from: From = FromExports
}

final case class DynamicImportWithoutModuleSupport(from: From) extends Error

sealed trait From
final case class FromMethod(methodInfo: MethodInfo) extends From
final case class FromClass(classInfo: ClassInfo) extends From
Expand Down Expand Up @@ -256,6 +259,8 @@ object Analysis {
case MultiplePublicModulesWithoutModuleSupport(moduleIDs) =>
"Found multiple public modules but module support is disabled: " +
moduleIDs.map(_.id).mkString("[", ", ", "]")
case DynamicImportWithoutModuleSupport(_) =>
"Uses dynamic import but module support is disabled"
}

logger.log(level, headMsg)
Expand Down

0 comments on commit 2929537

Please sign in to comment.