Navigation Menu

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

Async Module Loading / Progressive App Support #4201

Closed
gzm0 opened this issue Sep 16, 2020 · 4 comments · Fixed by #4262
Closed

Async Module Loading / Progressive App Support #4201

gzm0 opened this issue Sep 16, 2020 · 4 comments · Fixed by #4262
Assignees
Labels
enhancement Feature request (that does not concern language semantics, see "language")
Milestone

Comments

@gzm0
Copy link
Contributor

gzm0 commented Sep 16, 2020

Follow-up after #2681 / #4198 to support async module loading for true progressive app support

@gzm0 gzm0 added the enhancement Feature request (that does not concern language semantics, see "language") label Sep 16, 2020
@gzm0
Copy link
Contributor Author

gzm0 commented Sep 24, 2020

An idea that is floating in my head is to introduce an IR node Async (or Lazy) taking a special set of nodes (list see below). The node would return the original result in js.Promise after lazy loading the relevant class.

Nodes:

  • New
  • LoadModule
  • SelectStatic
  • SelectJSNativeMember
  • ApplyStatic
  • ? ApplyStatically (only used in super ctor)
  • LoadJSConstructor
  • LoadJSModule
  • CreateJSClass
  • ? IsInstanceOf (probably not useful)
  • ? AsInstanceOf (probably not useful)

@sjrd
Copy link
Member

sjrd commented Sep 24, 2020

Coming from the source language side, I had the following in mind. Add a primitive method defined as:

package object js {
  def dynamicImport[A](body: => A): js.Promise[A] = <stub>
}

which can be used like

class Foo(val x: Int) {
  def square: Int = x * x
}

class Bar {
  def work(x: Int): Unit = {
    val result = js.dynamicImport {
      new Foo(x).square
    }
    for (r <- result.toFuture)
      println(s"square is $r")
  }
}

The semantics would be that the js.dynamicImport { ... } block delimits a module boundary, that can be asynchronously loaded. When we get to that "function call", we perform a dynamic import() of the module that contains the body, and when the module is resolved, we chain it with invoking the body (passing any necessary captured variable like x). In JavaScript, that would look like

// dyn-module-1.js
class $c_Foo { ... }

export default function(x) {
  return new $c_Foo().init___I(x).square__I()
}
// main.js
class $c_Bar {
  ...
  work__I__V(x) {
    const result = import("./dyn-module-1.js")
      .then(mod => mod.default(x));
    result.then(r => console.log(`square is ${r}`));
  }
}

How does it work?

In practice, the body of a js.dynamicImport { body } is extracted in a static method, taking the free variables of body as formal parameters.

class Bar$Async$1 {
  static def entryPoint__int__java.lang.Object(x: int): any = {
    new $c_Foo().init___I(x).square__I();
  }
}

At the "call site", we leave an indication that there is a dynamic import block, mentioning the static method's full name + actual capture values. In terms of IR, we would only need one node of the form:

case class AsyncStaticApply(cls: ClassName, methodName: MethodName, args: List[Tree]) extends Tree {
  val tpe = AnyType
}

which could print as

async Bar$Async$1::entryPoint__int__java.lang.Object(x)

and which semantically simply means: asynchronously call that static method with the given arguments, returning a Promise of the result.

That semantics allows the linker to implement it as:

  1. forcefully put cls::methodName as default export in a synthetic module
  2. perform a dynamic import of that module and chain it with a call to that exported method, passing args as parameters.

The args must typecheck as appropriate arguments for the methodName. The result type of the method must be any.

@gzm0
Copy link
Contributor Author

gzm0 commented Sep 25, 2020

Hah, that's a neat solution. Unsure though why it should be a default export and not just simply a plain old boring static method.

Also, I reckon AsyncStaticApply needs ApplyFlags as well?

@sjrd
Copy link
Member

sjrd commented Sep 25, 2020

Unsure though why it should be a default export and not just simply a plain old boring static method.

Oh yes, sure, it can be exported under any name. But it needs to be exported, in order to be accessible in the .then(mod => mod.theAppropriateStaticMethod(...args). The name can be chosen by the linker as it sees fit.

Also, I reckon AsyncStaticApply needs ApplyFlags as well?

Ah yes, indeed.

@gzm0 gzm0 self-assigned this Sep 27, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 20, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 20, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 21, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 22, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 22, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Oct 23, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 3, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 3, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 4, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 8, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 9, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 9, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 9, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 10, 2020
@gzm0 gzm0 added this to the v1.4.0 milestone Nov 13, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 20, 2020
TODO:
- Rename to loadDynamic?
- Ensure that we can optimize dynamic native imports
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 23, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 23, 2020
@gzm0 gzm0 linked a pull request Nov 24, 2020 that will close this issue
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 25, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Nov 26, 2020
gzm0 added a commit to gzm0/scala-js that referenced this issue Dec 3, 2020
@gzm0 gzm0 closed this as completed in #4262 Dec 4, 2020
gzm0 added a commit that referenced this issue Dec 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Feature request (that does not concern language semantics, see "language")
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants