Skip to content

Support Scala.js #127

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

Merged
merged 17 commits into from
May 1, 2025
Merged

Support Scala.js #127

merged 17 commits into from
May 1, 2025

Conversation

natsukagami
Copy link
Contributor

@natsukagami natsukagami commented Apr 30, 2025

Currently using nightly Scala 3 for support for scala.js 1.19.
JSPI async/await is provided with a shim and manual transformation, until they gain compiler support.

  • Async.blocking is not really available unless we have top-level await or assume an existing js.async scope.
    To remedy this, Async.blocking now requires an instanace of the sealed Async.FromSync trait, which can be
    provided (and is provided by default) on JVM and Native by initializing FromSync.BlockingWithLocks with
    AsyncSupport and Scheduler as before.

    Specifically, the Async.blocking function requires an instance of the FromSync.Blocking trait, which refines
    FromSync with the requirement that the return type when running an asynchronous block is T.
    This is not always possible on Scala.js for example, where the default FromSync instance given would return
    scala.concurrent.Future[T] instead.

    There are two ways to get around this and have a top-level program to run on all 3 platforms:

    1. Use Async.fromSync, which is the same as Async.blocking but without the Output[T] = T requirement.
      One has to be careful that on Scala.js, this would return a Future[T], and the computation might have not
      completed by the time fromSync returns.

    2. To instead have the blocking behavior, assuming top-level await or an existing js.async scope, provide
      an instance of FromSync.Blocking with the js.UnsafeJsAsyncFromSync implementation.

      import gears.async.*
      
      given Async.FromSync.Blocking = js.UnsafeJsAsyncFromSync`
  • Because of the single-threaded nature of JS, listener locks have to be able to suspend an asynchronous computation
    instead of blocking the whole thread as possible on JVM/Native. Therefore, NumberedLock on scala.js implicitly
    assumes top-level await or an existing js.async scope.

    This works nicely with Future and Promises in Gears, as well as interacting directly with JavaScript promises,
    however manually written Sources should be aware of this limitation.

  • Writing spinning-like code under a Future may directly block the entire computation. This is not a direct limitation
    on JS alone (this will also saturate a carrier thread on JVM/Native as well), but has a much more direct effect on
    scala.js due to only having a single thread.

@natsukagami natsukagami marked this pull request as draft April 30, 2025 16:27
@@ -30,7 +30,7 @@ import scala.util.boundary
* @see
* [[Async$.group Async.group]] and [[Future$.apply Future.apply]] for [[Async]]-subscoping operations.
*/
trait Async(using val support: AsyncSupport, val scheduler: support.Scheduler):
trait Async private[async] (using val support: AsyncSupport, val scheduler: support.Scheduler):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make it sealed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide an adapter for extension point ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking about whether Async should be extensible or not. By not doing it, we retain more power to reason about it. It will also make it easier to add more to it without breaking changes (for example, context-local data)

@natsukagami
Copy link
Contributor Author

Hmm, stress test on CI is much slower on my machine (30s / 80s), should look into it.

)
)

def patchJSPIIR(jspiIRFile: File, streams: TaskStreams): Unit = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is only to support js.async/await until the Scala 3 compiler natively supports it

@natsukagami natsukagami marked this pull request as ready for review May 1, 2025 15:44
Copy link
Contributor Author

@natsukagami natsukagami left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance is a bit worrying but I think it is ok for merging. I will keep an eye on perf as we iterate.

@@ -41,3 +43,103 @@ lazy val root =
}
)
)
.jsSettings(
Seq(
scalaVersion := "3.7.1-RC1-bin-20250425-fb6cc9b-NIGHTLY",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine to keep this as-is for now, until 3.7.0 drops.

Resolves infinite initialization loop. We need to check for its nullness when doing a re-link.
@natsukagami natsukagami merged commit 539d1e2 into lampepfl:main May 1, 2025
4 checks passed
@natsukagami natsukagami deleted the scala-js branch May 1, 2025 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants