-
Notifications
You must be signed in to change notification settings - Fork 31
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
Support Scala.js #127
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
14aceae
Add scalajs 1.19 with nightly Scala
natsukagami cbb7a08
Introduce a trait for spawning Async.blocking
natsukagami 878b5c0
Add WASM JSPI-based SuspendSupport implementation for js
natsukagami 9105502
Lots of patches to get tests to compile and run
natsukagami 06948ac
Run `AsyncLocker` in a single-threaded context
natsukagami 971ae57
Add CI to test Gears on Scala.js
natsukagami c191387
Put a long timeout on Stress test
natsukagami abba3b4
Add documentation to WasmSuspend implementation
natsukagami efbeee3
Do not wrap `resolve` under another function
natsukagami c672a3e
Move state constants outside of AwaitListener
natsukagami 6df48dc
Make sure all tests with Async.blocking returns the future
natsukagami a5920dc
Drop stack requirements from jsEnv
natsukagami 08fd8de
Rename current `Async.blocking` to `fromSync`, add `blocking` that ac…
natsukagami 165e977
Move CountdownLatch to examples
natsukagami fc8c828
Remove debugging printlns from tests
natsukagami 6234f87
Create a State type and move comments to it
natsukagami 149a34f
Don't automatically initialize Cancellable.group to Unlinked
natsukagami File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import org.scalajs.jsenv.nodejs._ | ||
import org.scalajs.linker.interface.ESVersion | ||
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} | ||
import scalanative.build._ | ||
|
||
|
@@ -18,7 +20,7 @@ inThisBuild( | |
) | ||
|
||
lazy val root = | ||
crossProject(JVMPlatform, NativePlatform) | ||
crossProject(JSPlatform, JVMPlatform, NativePlatform) | ||
.crossType(CrossType.Full) | ||
.in(file(".")) | ||
.settings( | ||
|
@@ -41,3 +43,103 @@ lazy val root = | |
} | ||
) | ||
) | ||
.jsSettings( | ||
Seq( | ||
scalaVersion := "3.7.1-RC1-bin-20250425-fb6cc9b-NIGHTLY", | ||
// Emit ES modules with the Wasm backend | ||
scalaJSLinkerConfig := { | ||
scalaJSLinkerConfig.value | ||
.withESFeatures(_.withESVersion(ESVersion.ES2017)) // enable async/await | ||
.withExperimentalUseWebAssembly(true) // use the Wasm backend | ||
.withModuleKind(ModuleKind.ESModule) // required by the Wasm backend | ||
}, | ||
// Configure Node.js (at least v23) to support the required Wasm features | ||
jsEnv := { | ||
val config = NodeJSEnv | ||
.Config() | ||
.withArgs( | ||
List( | ||
"--experimental-wasm-exnref", // always required | ||
"--experimental-wasm-jspi", // required for js.async/js.await | ||
"--experimental-wasm-imported-strings", // optional (good for performance) | ||
"--turboshaft-wasm" // optional, but significantly increases stability | ||
) | ||
) | ||
new NodeJSEnv(config) | ||
}, | ||
|
||
// Patch the sjsir of JSPI to introduce primitives by hand | ||
Compile / compile := { | ||
val analysis = (Compile / compile).value | ||
|
||
val s = streams.value | ||
val classDir = (Compile / classDirectory).value | ||
val jspiIRFile = classDir / "gears/async/js/JSPI$.sjsir" | ||
patchJSPIIR(jspiIRFile, s) | ||
|
||
analysis | ||
} | ||
) | ||
) | ||
|
||
def patchJSPIIR(jspiIRFile: File, streams: TaskStreams): Unit = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that this is only to support |
||
import org.scalajs.ir.Names._ | ||
import org.scalajs.ir.Trees._ | ||
import org.scalajs.ir.Types._ | ||
import org.scalajs.ir.WellKnownNames._ | ||
|
||
val content = java.nio.ByteBuffer.wrap(java.nio.file.Files.readAllBytes(jspiIRFile.toPath())) | ||
val classDef = org.scalajs.ir.Serializers.deserialize(content) | ||
|
||
val newMethods = classDef.methods.mapConserve { m => | ||
(m.methodName.simpleName.nameString, m.body) match { | ||
case ("async", Some(UnaryOp(UnaryOp.Throw, _))) => | ||
implicit val pos = m.pos | ||
val closure = Closure( | ||
ClosureFlags.arrow.withAsync(true), | ||
m.args, | ||
Nil, | ||
None, | ||
AnyType, | ||
Apply(ApplyFlags.empty, m.args.head.ref, MethodIdent(MethodName("apply", Nil, ObjectRef)), Nil)(AnyType), | ||
m.args.map(_.ref) | ||
) | ||
val newBody = Some(JSFunctionApply(closure, Nil)) | ||
m.copy(body = newBody)(m.optimizerHints, m.version)(m.pos) | ||
|
||
case ("await", Some(UnaryOp(UnaryOp.Throw, _))) => | ||
implicit val pos = m.pos | ||
val newBody = Some(JSAwait(m.args.head.ref)) | ||
m.copy(body = newBody)(m.optimizerHints, m.version)(m.pos) | ||
|
||
case _ => | ||
m | ||
} | ||
} | ||
|
||
if (newMethods ne classDef.methods) { | ||
streams.log.info("Patching JSPI$.sjsir") | ||
val newClassDef = { | ||
import classDef._ | ||
ClassDef( | ||
name, | ||
originalName, | ||
kind, | ||
jsClassCaptures, | ||
superClass, | ||
interfaces, | ||
jsSuperClass, | ||
jsNativeLoadSpec, | ||
fields, | ||
newMethods, | ||
jsConstructor, | ||
jsMethodProps, | ||
jsNativeMembers, | ||
topLevelExportDefs | ||
)(optimizerHints)(pos) | ||
} | ||
val baos = new java.io.ByteArrayOutputStream() | ||
org.scalajs.ir.Serializers.serialize(baos, newClassDef) | ||
java.nio.file.Files.write(jspiIRFile.toPath(), baos.toByteArray()) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package gears.async | ||
|
||
private[async] abstract class AsyncImpl |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package gears.async.js | ||
|
||
import scala.scalajs.js | ||
import scala.scalajs.js.annotation.* | ||
|
||
/** A stub implementation of the [[js.async]]/[[js.await]] functions, which are not yet available natively on Scala 3. | ||
* | ||
* See how the stubs are compiled in `build.sbt`. | ||
*/ | ||
private[async] object JSPI: | ||
@inline | ||
def async[A](computation: => A): js.Promise[A] = | ||
throw new Error("async stub") | ||
|
||
@inline | ||
def await[A](p: js.Promise[A]): A = | ||
throw new Error("await stub") | ||
end JSPI |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package gears.async | ||
|
||
import gears.async.js.JSPI | ||
|
||
import java.util.concurrent.TimeUnit | ||
import java.util.concurrent.locks.* | ||
import scala.concurrent.duration.* | ||
import scala.scalajs.js | ||
|
||
/** Assumes top-level await, probably will crash real hard. */ | ||
private[async] trait NumberedLockImpl: | ||
protected val numberedLock = new Lock: | ||
var locked = false | ||
val queue = scala.collection.mutable.Queue[(Unit) => Any]() | ||
|
||
override def newCondition(): Condition = | ||
throw NotImplementedError() | ||
|
||
override def tryLock(): Boolean = | ||
if locked then false | ||
else | ||
locked = true | ||
true | ||
|
||
override def tryLock(time: Long, unit: TimeUnit): Boolean = | ||
throw NotImplementedError() | ||
|
||
override def lock(): Unit = | ||
while !tryLock() do | ||
val promise = js.Promise[Unit]: (resolve, _) => | ||
queue += resolve | ||
JSPI.await(promise) | ||
|
||
override def unlock(): Unit = | ||
assert(locked, "unlocking an unlocked lock") | ||
locked = false | ||
if !queue.isEmpty then queue.dequeue()(()) | ||
|
||
override def lockInterruptibly(): Unit = | ||
throw NotImplementedError() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.