Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #15701: Implement js.dynamicImport for dynamic module loading. #15720

Merged
merged 4 commits into from
Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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