Skip to content

Initial implementation of the WebAssembly backend. #4988

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 4 commits into from
Aug 10, 2024
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
38 changes: 38 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,41 @@ def Tasks = [
++$scala $testSuite$v/test
''',

"test-suite-webassembly": '''
setJavaVersion $java
npm install &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
helloworld$v/run &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
'set scalaJSStage in Global := FullOptStage' \
'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withPrettyPrint(true))' \
helloworld$v/run &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
reversi$v/fastLinkJS \
reversi$v/fullLinkJS &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \
'set scalaJSStage in Global := FullOptStage' jUnitTestOutputsJS$v/test testBridge$v/test &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
$testSuite$v/test &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
'set scalaJSStage in Global := FullOptStage' \
$testSuite$v/test &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
testingExample$v/testHtml &&
sbtretry ++$scala \
'set Global/enableWasmEverywhere := true' \
'set scalaJSStage in Global := FullOptStage' \
testingExample$v/testHtml
''',

/* For the bootstrap tests to be able to call
* `testSuite/test:fastOptJS`, `scalaJSStage in testSuite` must be
* `FastOptStage`, even when `scalaJSStage in Global` is `FullOptStage`.
Expand Down Expand Up @@ -539,8 +574,11 @@ mainScalaVersions.each { scalaVersion ->
quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"])
quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "true", testSuite: "testSuite"])
quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "testSuite"])
quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"])
quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuiteEx"])
quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"])
quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "scalaTestSuite"])
quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"])
quickMatrix.add([task: "bootstrap", scala: scalaVersion, java: mainJavaVersion])
quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion])
}
Expand Down
19 changes: 19 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ $ python3 -m http.server
// Open http://localhost:8000/test-suite/js/.2.12/target/scala-2.12/scalajs-test-suite-fastopt-test-html/index.html
```

## HTML-Test Runner with WebAssembly

WebAssembly requires modules, so this is manual as well.

This test currently requires Chrome (or another V8-based browser) with `--wasm-experimental-exnref` enabled.
That option can be configured as "Experimental WebAssembly" at [chrome://flags/#enable-experimental-webassembly-features](chrome://flags/#enable-experimental-webassembly-features).

```
$ sbt
> set Global/enableWasmEverywhere := true
> testingExample2_12/testHtml
> testSuite2_12/testHtml
> exit
$ python3 -m http.server

// Open http://localhost:8000/examples/testing/.2.12/target/scala-2.12/testing-fastopt-test-html/index.html
// Open http://localhost:8000/test-suite/js/.2.12/target/scala-2.12/scalajs-test-suite-fastopt-test-html/index.html
```

## Sourcemaps

To test source maps, do the following on:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap
import scala.util.matching.Regex

object ScalaJSVersions extends VersionChecks(
current = "1.16.1-SNAPSHOT",
current = "1.17.0-SNAPSHOT",
binaryEmitted = "1.16"
)

Expand Down
5 changes: 4 additions & 1 deletion ir/shared/src/main/scala/org/scalajs/ir/UTF8String.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

package org.scalajs.ir

import java.nio.CharBuffer
import java.nio.{ByteBuffer, CharBuffer}
import java.nio.charset.CharacterCodingException
import java.nio.charset.CodingErrorAction
import java.nio.charset.StandardCharsets.UTF_8
Expand Down Expand Up @@ -48,6 +48,9 @@ final class UTF8String private (private[ir] val bytes: Array[Byte])
System.arraycopy(that.bytes, 0, result, thisLen, thatLen)
new UTF8String(result)
}

def writeTo(buffer: ByteBuffer): Unit =
buffer.put(bytes)
}

object UTF8String {
Expand Down
8 changes: 6 additions & 2 deletions javalib/src/main/scala/java/lang/StackTrace.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ private[lang] object StackTrace {
* prototypes.
*/
reference
} else if (js.constructorOf[js.Error].captureStackTrace eq ().asInstanceOf[AnyRef]) {
// Create a JS Error with the current stack trace.
} else if ((js.constructorOf[js.Error].captureStackTrace eq ().asInstanceOf[AnyRef]) ||
js.Object.isSealed(throwable.asInstanceOf[js.Object])) {
/* If `captureStackTrace` is not available, or if the `throwable` instance
* is sealed (which notably happens on Wasm), create a JS `Error` with the
* current stack trace.
*/
new js.Error()
} else {
/* V8-specific.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ final class StandardConfig private (
* On the JavaScript platform, this does not have any effect.
*/
val closureCompilerIfAvailable: Boolean,
/** Pretty-print the output. */
/** Pretty-print the output, for debugging purposes.
*
* For the WebAssembly backend, this results in an additional `.wat` file
* next to each produced `.wasm` file with the WebAssembly text format
* representation of the latter. This file is never subsequently used,
* but may be inspected for debugging pruposes.
*/
val prettyPrint: Boolean,
/** Whether the linker should run in batch mode.
*
Expand All @@ -78,7 +84,9 @@ final class StandardConfig private (
*/
val batchMode: Boolean,
/** The maximum number of (file) writes executed concurrently. */
val maxConcurrentWrites: Int
val maxConcurrentWrites: Int,
/** If true, use the experimental WebAssembly backend. */
val experimentalUseWebAssembly: Boolean
) {
private def this() = {
this(
Expand All @@ -97,7 +105,8 @@ final class StandardConfig private (
closureCompilerIfAvailable = false,
prettyPrint = false,
batchMode = false,
maxConcurrentWrites = 50
maxConcurrentWrites = 50,
experimentalUseWebAssembly = false
)
}

Expand Down Expand Up @@ -177,6 +186,40 @@ final class StandardConfig private (
def withMaxConcurrentWrites(maxConcurrentWrites: Int): StandardConfig =
copy(maxConcurrentWrites = maxConcurrentWrites)

/** Specifies whether to use the experimental WebAssembly backend.
*
* When using this setting, the following settings must also be set:
*
* - `withSemantics(sems)` such that the behaviors of `sems` are all set to
* `CheckedBehavior.Unchecked`
* - `withModuleKind(ModuleKind.ESModule)`
* - `withOptimizer(false)`
* - `withStrictFloats(true)` (this is the default)
*
* These restrictions will be lifted in the future, except for the
* `ModuleKind`.
*
* If any of these restrictions are not met, linking will eventually throw
* an `IllegalArgumentException`.
*
* @note
* Currently, the WebAssembly backend silently ignores `@JSExport` and
* `@JSExportAll` annotations. This behavior may change in the future,
* either by making them warnings or errors, or by adding support for them.
* All other language features are supported.
*
* @note
* This setting is experimental. It may be removed in an upcoming *minor*
* version of Scala.js. Future minor versions may also produce code that
* requires more recent versions of JS engines supporting newer WebAssembly
* standards.
*
* @throws java.lang.UnsupportedOperationException
* In the future, if the feature gets removed.
*/
def withExperimentalUseWebAssembly(experimentalUseWebAssembly: Boolean): StandardConfig =
copy(experimentalUseWebAssembly = experimentalUseWebAssembly)

override def toString(): String = {
s"""StandardConfig(
| semantics = $semantics,
Expand All @@ -195,6 +238,7 @@ final class StandardConfig private (
| prettyPrint = $prettyPrint,
| batchMode = $batchMode,
| maxConcurrentWrites = $maxConcurrentWrites,
| experimentalUseWebAssembly = $experimentalUseWebAssembly,
|)""".stripMargin
}

Expand All @@ -214,7 +258,8 @@ final class StandardConfig private (
closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable,
prettyPrint: Boolean = prettyPrint,
batchMode: Boolean = batchMode,
maxConcurrentWrites: Int = maxConcurrentWrites
maxConcurrentWrites: Int = maxConcurrentWrites,
experimentalUseWebAssembly: Boolean = experimentalUseWebAssembly
): StandardConfig = {
new StandardConfig(
semantics,
Expand All @@ -232,7 +277,8 @@ final class StandardConfig private (
closureCompilerIfAvailable,
prettyPrint,
batchMode,
maxConcurrentWrites
maxConcurrentWrites,
experimentalUseWebAssembly
)
}
}
Expand Down Expand Up @@ -263,6 +309,7 @@ object StandardConfig {
.addField("prettyPrint", config.prettyPrint)
.addField("batchMode", config.batchMode)
.addField("maxConcurrentWrites", config.maxConcurrentWrites)
.addField("experimentalUseWebAssembly", config.experimentalUseWebAssembly)
.build()
}
}
Expand Down Expand Up @@ -290,6 +337,7 @@ object StandardConfig {
* - `prettyPrint`: `false`
* - `batchMode`: `false`
* - `maxConcurrentWrites`: `50`
* - `experimentalUseWebAssembly`: `false`
*/
def apply(): StandardConfig = new StandardConfig()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ package org.scalajs.linker.backend
private[backend] object LinkerBackendImplPlatform {
import LinkerBackendImpl.Config

def createLinkerBackend(config: Config): LinkerBackendImpl =
def createJSLinkerBackend(config: Config): LinkerBackendImpl =
new BasicLinkerBackend(config)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.scalajs.linker.backend.closure.ClosureLinkerBackend
private[backend] object LinkerBackendImplPlatform {
import LinkerBackendImpl.Config

def createLinkerBackend(config: Config): LinkerBackendImpl = {
def createJSLinkerBackend(config: Config): LinkerBackendImpl = {
if (config.closureCompiler)
new ClosureLinkerBackend(config)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ abstract class LinkerBackendImpl(
}

object LinkerBackendImpl {
def apply(config: Config): LinkerBackendImpl =
LinkerBackendImplPlatform.createLinkerBackend(config)
def apply(config: Config): LinkerBackendImpl = {
if (config.experimentalUseWebAssembly)
new WebAssemblyLinkerBackend(config)
else
LinkerBackendImplPlatform.createJSLinkerBackend(config)
}

/** Configurations relevant to the backend */
final class Config private (
Expand All @@ -62,7 +66,9 @@ object LinkerBackendImpl {
/** Pretty-print the output. */
val prettyPrint: Boolean,
/** The maximum number of (file) writes executed concurrently. */
val maxConcurrentWrites: Int
val maxConcurrentWrites: Int,
/** If true, use the experimental WebAssembly backend. */
val experimentalUseWebAssembly: Boolean
) {
private def this() = {
this(
Expand All @@ -74,7 +80,9 @@ object LinkerBackendImpl {
minify = false,
closureCompilerIfAvailable = false,
prettyPrint = false,
maxConcurrentWrites = 50)
maxConcurrentWrites = 50,
experimentalUseWebAssembly = false
)
}

def withCommonConfig(commonConfig: CommonPhaseConfig): Config =
Expand Down Expand Up @@ -106,6 +114,9 @@ object LinkerBackendImpl {
def withMaxConcurrentWrites(maxConcurrentWrites: Int): Config =
copy(maxConcurrentWrites = maxConcurrentWrites)

def withExperimentalUseWebAssembly(experimentalUseWebAssembly: Boolean): Config =
copy(experimentalUseWebAssembly = experimentalUseWebAssembly)

private def copy(
commonConfig: CommonPhaseConfig = commonConfig,
jsHeader: String = jsHeader,
Expand All @@ -115,7 +126,9 @@ object LinkerBackendImpl {
minify: Boolean = minify,
closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable,
prettyPrint: Boolean = prettyPrint,
maxConcurrentWrites: Int = maxConcurrentWrites): Config = {
maxConcurrentWrites: Int = maxConcurrentWrites,
experimentalUseWebAssembly: Boolean = experimentalUseWebAssembly
): Config = {
new Config(
commonConfig,
jsHeader,
Expand All @@ -125,7 +138,8 @@ object LinkerBackendImpl {
minify,
closureCompilerIfAvailable,
prettyPrint,
maxConcurrentWrites
maxConcurrentWrites,
experimentalUseWebAssembly
)
}
}
Expand Down
Loading