Skip to content
This repository was archived by the owner on Jul 12, 2024. It is now read-only.

Commit 0f44ca0

Browse files
authored
Merge pull request #96 from sjrd/finish-top-level-exports
Implement support for the remaining top-level exports.
2 parents 254e6c5 + 42137f1 commit 0f44ca0

File tree

8 files changed

+145
-18
lines changed

8 files changed

+145
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package testsuite.core
2+
3+
import scala.scalajs.js
4+
import scala.scalajs.js.annotation._
5+
6+
object JSExportTopLevelTest {
7+
def main(): Unit = {
8+
/* The main() function of this test does nothing.
9+
* The real test happens after loading the module from TestSuites.scala.
10+
*/
11+
()
12+
}
13+
14+
@JSExportTopLevel("immutableField")
15+
val immutableField: String = "my immutable field value"
16+
17+
@JSExportTopLevel("mutableField")
18+
var mutableField: Int = 42
19+
20+
@JSExportTopLevel("simpleFunction")
21+
def simpleFunction(x: Int): Int =
22+
x * x
23+
24+
@JSExportTopLevel("functionWithRest")
25+
def functionWithRest(x: Int, rest: Int*): Int =
26+
x * rest.sum
27+
28+
@JSExportTopLevel("SimpleClass")
29+
class SimpleClass(val foo: Int) extends js.Object
30+
31+
@JSExportTopLevel("SimpleObject")
32+
object SimpleObject extends js.Object {
33+
val bar: String = "the bar field"
34+
}
35+
}

Diff for: tests/src/test/scala/tests/CoreTests.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ class CoreTests extends munit.FunSuite {
7575
for {
7676
irFiles <- irFilesFuture
7777
linkerResult <- linker.link(irFiles, moduleInitializers, output, logger)
78-
runResult <- js.`import`[js.Any](s"$outputDirRelToMyJSFile/main.mjs").toFuture
78+
moduleExports <- js.`import`[js.Dynamic](s"$outputDirRelToMyJSFile/main.mjs").toFuture
7979
} yield {
80-
()
80+
for (postLoadTests <- suite.postLoadTests)
81+
postLoadTests(moduleExports)
8182
}
8283
}
8384
}

Diff for: tests/src/test/scala/tests/TestSuites.scala

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
package tests
22

3+
import scala.scalajs.js
4+
35
object TestSuites {
4-
case class TestSuite(className: String, methodName: String = "main")
6+
case class TestSuite(
7+
className: String,
8+
methodName: String = "main",
9+
postLoadTests: Option[js.Dynamic => Unit] = None
10+
)
11+
512
val suites = List(
613
TestSuite("testsuite.core.Simple"),
714
TestSuite("testsuite.core.Add"),
@@ -38,6 +45,21 @@ object TestSuites {
3845
TestSuite("testsuite.core.MatchTest"),
3946
TestSuite("testsuite.core.WrapUnwrapThrowableTest"),
4047
TestSuite("testsuite.core.StringEncodingTest"),
41-
TestSuite("testsuite.core.ReflectiveCallTest")
48+
TestSuite("testsuite.core.ReflectiveCallTest"),
49+
TestSuite(
50+
"testsuite.core.JSExportTopLevelTest",
51+
postLoadTests = Some({ moduleExports =>
52+
import munit.Assertions.assert
53+
54+
assert((moduleExports.immutableField: Any) == "my immutable field value")
55+
assert((moduleExports.mutableField: Any) == 42)
56+
assert((moduleExports.simpleFunction(5): Any) == 25)
57+
assert((moduleExports.functionWithRest(3, 5, 6, 7): Any) == 54)
58+
assert((moduleExports.SimpleObject.bar: Any) == "the bar field")
59+
60+
val obj = js.Dynamic.newInstance(moduleExports.SimpleClass)(456)
61+
assert((obj.foo: Any) == 456)
62+
})
63+
)
4264
)
4365
}

Diff for: wasm/src/main/scala/WebAssemblyLinkerBackend.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ final class WebAssemblyLinkerBackend(
101101
val classesWithStaticInit =
102102
sortedClasses.filter(_.hasStaticInitializer).map(_.className)
103103

104-
context.complete(onlyModule.initializers.toList, classesWithStaticInit)
104+
context.complete(
105+
onlyModule.initializers.toList,
106+
classesWithStaticInit,
107+
onlyModule.topLevelExports
108+
)
105109

106110
val outputImpl = OutputDirectoryImpl.fromOutputDirectory(output)
107111

Diff for: wasm/src/main/scala/ir2wasm/LoaderContent.scala

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const scalaJSHelpers = {
112112
closureThis: (f, data) => function(...args) { return f(data, this, ...args); },
113113
closureRest: (f, data, n) => ((...args) => f(data, ...args.slice(0, n), args.slice(n))),
114114
closureThisRest: (f, data, n) => function(...args) { return f(data, this, ...args.slice(0, n), args.slice(n)); },
115+
closureRestNoData: (f, n) => ((...args) => f(...args.slice(0, n), args.slice(n))),
115116

116117
// Strings
117118
emptyString: () => "",

Diff for: wasm/src/main/scala/ir2wasm/WasmBuilder.scala

+37-10
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ class WasmBuilder {
143143
topLevelExport: LinkedTopLevelExport
144144
)(implicit ctx: WasmContext): Unit = {
145145
topLevelExport.tree match {
146-
case d: IRTrees.TopLevelJSClassExportDef => ???
147-
case d: IRTrees.TopLevelModuleExportDef => ???
146+
case d: IRTrees.TopLevelJSClassExportDef => genDelayedTopLevelExport(d.exportName)
147+
case d: IRTrees.TopLevelModuleExportDef => genDelayedTopLevelExport(d.exportName)
148148
case d: IRTrees.TopLevelMethodExportDef => transformTopLevelMethodExportDef(d)
149149
case d: IRTrees.TopLevelFieldExportDef => transformTopLevelFieldExportDef(d)
150150
}
@@ -928,25 +928,26 @@ class WasmBuilder {
928928
val method = exportDef.methodDef
929929
val exportedName = exportDef.topLevelExportName
930930

931-
if (method.restParam.isDefined) {
932-
throw new UnsupportedOperationException(
933-
s"Top-level export with ...rest param is unsupported at ${method.pos}: $method"
934-
)
935-
}
936-
937931
implicit val fctx = WasmFunctionContext(
938932
enclosingClassName = None,
939933
Names.WasmFunctionName.forExport(exportedName),
940934
receiverTyp = None,
941-
method.args,
935+
method.args ::: method.restParam.toList,
942936
IRTypes.AnyType
943937
)
944938

945939
WasmExpressionBuilder.generateIRBody(method.body, IRTypes.AnyType)
946940

947941
val func = fctx.buildAndAddToContext()
948942

949-
ctx.addExport(WasmExport.Function(exportedName, func.name))
943+
if (method.restParam.isEmpty) {
944+
ctx.addExport(WasmExport.Function(exportedName, func.name))
945+
} else {
946+
/* We cannot directly export the function. We will create a closure
947+
* wrapper in the start function and export that instead.
948+
*/
949+
genDelayedTopLevelExport(exportedName)
950+
}
950951
}
951952

952953
private def transformTopLevelFieldExportDef(
@@ -959,6 +960,32 @@ class WasmBuilder {
959960
ctx.addExport(exprt)
960961
}
961962

963+
/** Generates a delayed top-level export global, to be initialized in the `start` function.
964+
*
965+
* Some top-level exports need to be initialized by run-time code because they need to call
966+
* initializing functions:
967+
*
968+
* - methods with a `...rest` need to be initialized with the `closureRestNoArg` helper.
969+
* - JS classes need to be initialized with their `loadJSClass` helper.
970+
* - JS modules need to be initialized with their `loadModule` helper.
971+
*
972+
* For all of those, we use `genDelayedTopLevelExport` to generate a Wasm global initialized with
973+
* `null` and to export it. We actually initialize the global in the `start` function (see
974+
* `genStartFunction()` in `WasmContext`).
975+
*/
976+
private def genDelayedTopLevelExport(exportedName: String)(implicit ctx: WasmContext): Unit = {
977+
val globalName = WasmGlobalName.forTopLevelExport(exportedName)
978+
ctx.addGlobal(
979+
WasmGlobal(
980+
globalName,
981+
WasmRefType.anyref,
982+
WasmExpr(List(REF_NULL(WasmHeapType.None))),
983+
isMutable = true
984+
)
985+
)
986+
ctx.addExport(WasmExport.Global(exportedName, globalName))
987+
}
988+
962989
private def genFunction(
963990
clazz: LinkedClass,
964991
method: IRTrees.MethodDef

Diff for: wasm/src/main/scala/wasm4s/Names.scala

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ object Names {
6868
def forStaticField(fieldName: IRNames.FieldName): WasmGlobalName =
6969
new WasmGlobalName(s"static.${fieldName.nameString}")
7070

71+
def forTopLevelExport(exportName: String): WasmGlobalName =
72+
new WasmGlobalName(s"export.$exportName")
73+
7174
def forJSPrivateField(fieldName: IRNames.FieldName): WasmGlobalName =
7275
new WasmGlobalName(s"jspfield.${fieldName.nameString}")
7376

@@ -169,6 +172,7 @@ object Names {
169172
val closureThis = helper("closureThis")
170173
val closureRest = helper("closureRest")
171174
val closureThisRest = helper("closureThisRest")
175+
val closureRestNoData = helper("closureRestNoData")
172176

173177
val emptyString = helper("emptyString")
174178
val stringLength = helper("stringLength")

Diff for: wasm/src/main/scala/wasm4s/WasmContext.scala

+36-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import wasm.ir2wasm.WasmExpressionBuilder
1919

2020
import org.scalajs.linker.interface.ModuleInitializer
2121
import org.scalajs.linker.interface.unstable.ModuleInitializerImpl
22+
import org.scalajs.linker.standard.LinkedTopLevelExport
23+
2224
import java.nio.charset.StandardCharsets
2325

2426
trait ReadOnlyWasmContext {
@@ -403,6 +405,11 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
403405
List(WasmRefType.func, anyref, WasmInt32),
404406
List(WasmRefType.any)
405407
)
408+
addHelperImport(
409+
WasmFunctionName.closureRestNoData,
410+
List(WasmRefType.func, WasmInt32),
411+
List(WasmRefType.any)
412+
)
406413

407414
addHelperImport(WasmFunctionName.emptyString, List(), List(WasmRefType.any))
408415
addHelperImport(WasmFunctionName.stringLength, List(WasmRefType.any), List(WasmInt32))
@@ -536,7 +543,8 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
536543

537544
def complete(
538545
moduleInitializers: List[ModuleInitializer.Initializer],
539-
classesWithStaticInit: List[IRNames.ClassName]
546+
classesWithStaticInit: List[IRNames.ClassName],
547+
topLevelExportDefs: List[LinkedTopLevelExport]
540548
): Unit = {
541549
/* Before generating the string globals in `genStartFunction()`, make sure
542550
* to allocate the ones that will be required by the module initializers.
@@ -566,13 +574,14 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
566574
)
567575
)
568576

569-
genStartFunction(moduleInitializers, classesWithStaticInit)
577+
genStartFunction(moduleInitializers, classesWithStaticInit, topLevelExportDefs)
570578
genDeclarativeElements()
571579
}
572580

573581
private def genStartFunction(
574582
moduleInitializers: List[ModuleInitializer.Initializer],
575-
classesWithStaticInit: List[IRNames.ClassName]
583+
classesWithStaticInit: List[IRNames.ClassName],
584+
topLevelExportDefs: List[LinkedTopLevelExport]
576585
): Unit = {
577586
import WasmInstr._
578587
import WasmTypeName._
@@ -638,6 +647,30 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
638647
instrs += WasmInstr.CALL(funcName)
639648
}
640649

650+
// Initialize the top-level exports that require it
651+
652+
for (tle <- topLevelExportDefs) {
653+
tle.tree match {
654+
case IRTrees.TopLevelJSClassExportDef(_, exportName) =>
655+
instrs += CALL(WasmFunctionName.loadJSClass(tle.owningClass))
656+
instrs += GLOBAL_SET(WasmGlobalName.forTopLevelExport(tle.exportName))
657+
case IRTrees.TopLevelModuleExportDef(_, exportName) =>
658+
instrs += CALL(WasmFunctionName.loadModule(tle.owningClass))
659+
instrs += GLOBAL_SET(WasmGlobalName.forTopLevelExport(tle.exportName))
660+
case IRTrees.TopLevelMethodExportDef(_, methodDef) =>
661+
// We only need initialization if there is a restParam
662+
if (methodDef.restParam.isDefined) {
663+
instrs += refFuncWithDeclaration(WasmFunctionName.forExport(tle.exportName))
664+
instrs += I32_CONST(methodDef.args.size)
665+
instrs += CALL(WasmFunctionName.closureRestNoData)
666+
instrs += GLOBAL_SET(WasmGlobalName.forTopLevelExport(tle.exportName))
667+
}
668+
case IRTrees.TopLevelFieldExportDef(_, _, _) =>
669+
// Nothing to do
670+
()
671+
}
672+
}
673+
641674
// Emit the module initializers
642675

643676
moduleInitializers.foreach { init =>

0 commit comments

Comments
 (0)