diff --git a/build.sbt b/build.sbt index 52e3be47..928d2a3d 100644 --- a/build.sbt +++ b/build.sbt @@ -184,6 +184,7 @@ lazy val `scalajs-test-suite` = project (Test / sources).value .filterNot(endsWith(_, "/UnionTypeTest.scala")) // requires typechecking macros .filterNot(endsWith(_, "/jsinterop/ExportsTest.scala")) // js.dynamicImport (multi-modules) + .filterNot(endsWith(_, "/ExportLoopback.scala")) }, Test / scalacOptions += "-P:scalajs:genStaticForwardersForNonTopLevelObjects", diff --git a/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala b/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala new file mode 100644 index 00000000..9fd1c8a9 --- /dev/null +++ b/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala @@ -0,0 +1,671 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.jsinterop + +import scala.scalajs.js +import scala.scalajs.js.annotation._ +import scala.scalajs.js.Dynamic.global + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.JSAssert._ +import org.scalajs.testsuite.utils.JSUtils +import org.scalajs.testsuite.utils.Platform._ + +import scala.annotation.meta + +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits.{global => globalEc} + +import org.junit.Assert._ +import org.junit.Assume._ +import org.junit.Test + +import org.scalajs.junit.async._ + +class TopLevelExportsTest { + + /** The namespace in which top-level exports are stored. */ + private lazy val exportsNamespace: Future[js.Dynamic] = + ExportLoopback.exportsNamespace + + def witnessOf(obj: Any): Any = { + assertTrue("" + obj.getClass(), obj.isInstanceOf[WitnessInterface]) + obj.asInstanceOf[WitnessInterface].witness + } + + // @JSExportTopLevel classes and objects + + @Test def toplevelExportsForObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.TopLevelExportedObject) + else exportsNamespace.map(_.TopLevelExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", witnessOf(obj)) + } + } + + @Test def toplevelExportsForScalaJSDefinedJSObjects(): AsyncResult = await { + val obj1Future = + if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedObject) + else exportsNamespace.map(_.SJSDefinedTopLevelExportedObject) + for (obj1 <- obj1Future) yield { + assertJSNotUndefined(obj1) + assertEquals("object", js.typeOf(obj1)) + assertEquals("witness", obj1.witness) + + assertSame(obj1, SJSDefinedExportedObject) + } + } + + @Test def toplevelExportsForNestedObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.NestedExportedObject) + else exportsNamespace.map(_.NestedExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertSame(obj, ExportHolder.ExportedObject) + } + } + + @Test def exportsForObjectsWithConstantFoldedName(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.ConstantFoldedObjectExport) + else exportsNamespace.map(_.ConstantFoldedObjectExport) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", witnessOf(obj)) + } + } + + @Test def exportsForProtectedObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.ProtectedExportedObject) + else exportsNamespace.map(_.ProtectedExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", witnessOf(obj)) + } + } + + @Test def toplevelExportsForClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.TopLevelExportedClass) + else exportsNamespace.map(_.TopLevelExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, witnessOf(obj)) + } + } + + @Test def toplevelExportsForScalaJSDefinedJSClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedClass) + else exportsNamespace.map(_.SJSDefinedTopLevelExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertTrue((obj: Any).isInstanceOf[SJSDefinedTopLevelExportedClass]) + assertEquals(5, obj.x) + + assertSame(constr, js.constructorOf[SJSDefinedTopLevelExportedClass]) + } + } + + @Test def toplevelExportsForAbstractJSClasses_Issue4117(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.TopLevelExportedAbstractJSClass) + else exportsNamespace.map(_.TopLevelExportedAbstractJSClass) + + for (constr <- constrFuture) yield { + assertEquals("function", js.typeOf(constr)) + + val body = if (useECMAScript2015Semantics) { + """ + class SubClass extends constr { + constructor(x) { + super(x); + } + foo(y) { + return y + this.x; + } + } + return SubClass; + """ + } else { + """ + function SubClass(x) { + constr.call(this, x); + } + SubClass.prototype = Object.create(constr.prototype); + SubClass.prototype.foo = function(y) { + return y + this.x; + }; + return SubClass; + """ + } + + val subclassFun = new js.Function("constr", body) + .asInstanceOf[js.Function1[js.Dynamic, js.Dynamic]] + val subclass = subclassFun(constr) + assertEquals("function", js.typeOf(subclass)) + + val obj = js.Dynamic + .newInstance(subclass)(5) + .asInstanceOf[TopLevelExportedAbstractJSClass] + + assertEquals(5, obj.x) + assertEquals(11, obj.foo(6)) + assertEquals(33, obj.bar(6)) + } + } + + @Test def toplevelExportsForNestedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.NestedExportedClass) + else exportsNamespace.map(_.NestedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)() + assertTrue((obj: Any).isInstanceOf[ExportHolder.ExportedClass]) + } + } + + @Test def toplevelExportsForNestedSjsDefinedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.NestedSJSDefinedExportedClass) + else exportsNamespace.map(_.NestedSJSDefinedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)() + assertTrue((obj: Any).isInstanceOf[ExportHolder.SJSDefinedExportedClass]) + } + } + + @Test def exportsForClassesWithConstantFoldedName(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ConstantFoldedClassExport) + else exportsNamespace.map(_.ConstantFoldedClassExport) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, witnessOf(obj)) + } + } + + @Test def exportsForProtectedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ProtectedExportedClass) + else exportsNamespace.map(_.ProtectedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, witnessOf(obj)) + } + } + + @Test def exportForClassesWithRepeatedParametersInCtor(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ExportedVarArgClass) + else exportsNamespace.map(_.ExportedVarArgClass) + for (constr <- constrFuture) yield { + assertEquals("", witnessOf(js.Dynamic.newInstance(constr)())) + assertEquals("a", witnessOf(js.Dynamic.newInstance(constr)("a"))) + assertEquals("a|b", witnessOf(js.Dynamic.newInstance(constr)("a", "b"))) + assertEquals("a|b|c", witnessOf(js.Dynamic.newInstance(constr)("a", "b", "c"))) + assertEquals("Number: <5>|a", witnessOf(js.Dynamic.newInstance(constr)(5, "a"))) + } + } + + @Test def exportForClassesWithDefaultParametersInCtor(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ExportedDefaultArgClass) + else exportsNamespace.map(_.ExportedDefaultArgClass) + for (constr <- constrFuture) yield { + assertEquals(6, witnessOf(js.Dynamic.newInstance(constr)(1, 2, 3))) + assertEquals(106, witnessOf(js.Dynamic.newInstance(constr)(1))) + assertEquals(103, witnessOf(js.Dynamic.newInstance(constr)(1, 2))) + } + } + + // @JSExportTopLevel methods + + @Test def basicTopLevelExport(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(1, global.TopLevelExport_basic()) + } + + @Test def basicTopLevelExportModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(1, exp.TopLevelExport_basic()) + } + } + + @Test def overloadedTopLevelExport(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_overload("World")) + assertEquals(2, global.TopLevelExport_overload(2)) + assertEquals(9, global.TopLevelExport_overload(2, 7)) + assertEquals(10, global.TopLevelExport_overload(1, 2, 3, 4)) + } + + @Test def overloadedTopLevelExportModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_overload("World")) + assertEquals(2, exp.TopLevelExport_overload(2)) + assertEquals(9, exp.TopLevelExport_overload(2, 7)) + assertEquals(10, exp.TopLevelExport_overload(1, 2, 3, 4)) + } + } + + @Test def defaultParamsTopLevelExport_Issue4052(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(7, global.TopLevelExport_defaultParams(6)) + assertEquals(11, global.TopLevelExport_defaultParams(6, 5)) + } + + @Test def defaultParamsTopLevelExportModule_Issue4052(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(7, exp.TopLevelExport_defaultParams(6)) + assertEquals(11, exp.TopLevelExport_defaultParams(6, 5)) + } + } + + @Test def topLevelExportUsesUniqueObject(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + global.TopLevelExport_set(3) + assertEquals(3, TopLevelExports.myVar) + global.TopLevelExport_set(7) + assertEquals(7, TopLevelExports.myVar) + } + + @Test def topLevelExportUsesUniqueObjectModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + exp.TopLevelExport_set(3) + assertEquals(3, TopLevelExports.myVar) + exp.TopLevelExport_set(7) + assertEquals(7, TopLevelExports.myVar) + } + } + + @Test def topLevelExportFromNestedObject(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + global.TopLevelExport_setNested(28) + assertEquals(28, TopLevelExports.Nested.myVar) + } + + @Test def topLevelExportFromNestedObjectModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + exp.TopLevelExport_setNested(28) + assertEquals(28, TopLevelExports.Nested.myVar) + } + } + + @Test def topLevelExportWithDoubleUnderscore(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(true, global.__topLevelExportWithDoubleUnderscore) + } + + @Test def topLevelExportWithDoubleUnderscoreModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(true, exp.__topLevelExportWithDoubleUnderscore) + } + } + + @Test def topLevelExportIsAlwaysReachable(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_reachability()) + } + + @Test def topLevelExportIsAlwaysReachableModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_reachability()) + } + } + + // @JSExportTopLevel fields + + @Test def topLevelExportBasicField(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + // Initialization + assertEquals(5, global.TopLevelExport_basicVal) + assertEquals("hello", global.TopLevelExport_basicVar) + + // Scala modifies var + TopLevelFieldExports.basicVar = "modified" + assertEquals("modified", TopLevelFieldExports.basicVar) + assertEquals("modified", global.TopLevelExport_basicVar) + + // Reset var + TopLevelFieldExports.basicVar = "hello" + } + + @Test def topLevelExportBasicFieldModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + // Initialization + assertEquals(5, exp.TopLevelExport_basicVal) + assertEquals("hello", exp.TopLevelExport_basicVar) + + // Scala modifies var + TopLevelFieldExports.basicVar = "modified" + assertEquals("modified", TopLevelFieldExports.basicVar) + assertEquals("modified", exp.TopLevelExport_basicVar) + + // Reset var + TopLevelFieldExports.basicVar = "hello" + } + } + + @Test def topLevelExportFieldTwice(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + + // Initialization + assertEquals(5, global.TopLevelExport_valExportedTwice1) + assertEquals("hello", global.TopLevelExport_varExportedTwice1) + assertEquals("hello", global.TopLevelExport_varExportedTwice2) + + // Scala modifies var + TopLevelFieldExports.varExportedTwice = "modified" + assertEquals("modified", TopLevelFieldExports.varExportedTwice) + assertEquals("modified", global.TopLevelExport_varExportedTwice1) + assertEquals("modified", global.TopLevelExport_varExportedTwice2) + + // Reset var + TopLevelFieldExports.varExportedTwice = "hello" + } + + @Test def topLevelExportFieldTwiceModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + // Initialization + assertEquals(5, exp.TopLevelExport_valExportedTwice1) + assertEquals("hello", exp.TopLevelExport_varExportedTwice1) + assertEquals("hello", exp.TopLevelExport_varExportedTwice2) + + // Scala modifies var + TopLevelFieldExports.varExportedTwice = "modified" + assertEquals("modified", TopLevelFieldExports.varExportedTwice) + assertEquals("modified", exp.TopLevelExport_varExportedTwice1) + assertEquals("modified", exp.TopLevelExport_varExportedTwice2) + + // Reset var + TopLevelFieldExports.varExportedTwice = "hello" + } + } + + @Test def topLevelExportWriteValVarCausesTypeerror(): AsyncResult = await { + assumeFalse("Unchecked in Script mode", isNoModule) + + for (exp <- exportsNamespace) yield { + assertThrows( + classOf[js.JavaScriptException], { + exp.TopLevelExport_basicVal = 54 + } + ) + + assertThrows( + classOf[js.JavaScriptException], { + exp.TopLevelExport_basicVar = 54 + } + ) + } + } + + @Test def topLevelExportUninitializedFieldsScala(): Unit = { + assertEquals(0, TopLevelFieldExports.uninitializedVarInt) + assertEquals(0L, TopLevelFieldExports.uninitializedVarLong) + assertEquals(null, TopLevelFieldExports.uninitializedVarString) + assertEquals('\u0000', TopLevelFieldExports.uninitializedVarChar) + } + + @Test def topLevelExportUninitializedFields(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(null, global.TopLevelExport_uninitializedVarInt) + assertEquals(null, global.TopLevelExport_uninitializedVarLong) + assertEquals(null, global.TopLevelExport_uninitializedVarString) + assertEquals(null, global.TopLevelExport_uninitializedVarChar) + } + + @Test def topLevelExportUninitializedFieldsModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(null, exp.TopLevelExport_uninitializedVarInt) + assertEquals(null, exp.TopLevelExport_uninitializedVarLong) + assertEquals(null, exp.TopLevelExport_uninitializedVarString) + assertEquals(null, exp.TopLevelExport_uninitializedVarChar) + } + } + + @Test def topLevelExportFieldIsAlwaysReachableAndInitialized(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_fieldreachability) + } + + @Test def topLevelExportFieldIsAlwaysReachableAndInitializedModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_fieldreachability) + } + } + + @Test def topLevelExportFieldIsWritableAccrossModules(): Unit = { + /* We write to basicVar exported above from a different object to test writing + * of static fields across module boundaries (when module splitting is + * enabled). + */ + + assertEquals("hello", TopLevelFieldExports.inlineVar) + TopLevelFieldExports.inlineVar = "hello modules" + assertEquals("hello modules", TopLevelFieldExports.inlineVar) + + // Reset var + TopLevelFieldExports.inlineVar = "hello" + } + + // @JSExportTopLevel in Script's are `let`s in ES 2015, `var`s in ES 5.1 + + @Test def topLevelExportsNoModuleAreOfCorrectKind(): Unit = { + assumeTrue("relevant only for NoModule", isNoModule) + + val g = JSUtils.globalObject + + // Do we expect to get undefined when looking up the exports in the global object? + val undefinedExpected = useECMAScript2015Semantics + + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedObject)) + assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedObject)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedClass)) + assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedClass)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basic)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVal)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVar)) + } +} + +object TopLevelExportNameHolder { + final val className = "ConstantFoldedClassExport" + final val objectName = "ConstantFoldedObjectExport" +} + +/** Access to a `witness` property in instances of exported Scala classes. */ +trait WitnessInterface { + def witness: Any +} + +@JSExportTopLevel("TopLevelExportedObject") +@JSExportTopLevel(TopLevelExportNameHolder.objectName) +object TopLevelExportedObject extends WitnessInterface { + val witness: String = "witness" +} + +@JSExportTopLevel("SJSDefinedTopLevelExportedObject") +object SJSDefinedExportedObject extends js.Object { + val witness: String = "witness" +} + +@JSExportTopLevel("ProtectedExportedObject") +protected object ProtectedExportedObject extends WitnessInterface { + def witness: String = "witness" +} + +@JSExportTopLevel("TopLevelExportedClass") +@JSExportTopLevel(TopLevelExportNameHolder.className) +class TopLevelExportedClass(_x: Int) extends WitnessInterface { + val witness = _x +} + +@JSExportTopLevel("SJSDefinedTopLevelExportedClass") +class SJSDefinedTopLevelExportedClass(val x: Int) extends js.Object + +@JSExportTopLevel("TopLevelExportedAbstractJSClass") +abstract class TopLevelExportedAbstractJSClass(val x: Int) extends js.Object { + def foo(y: Int): Int + + def bar(y: Int): Int = 3 * foo(y) +} + +@JSExportTopLevel("ProtectedExportedClass") +protected class ProtectedExportedClass(_x: Int) extends WitnessInterface { + val witness = _x +} + +@JSExportTopLevel("ExportedVarArgClass") +class ExportedVarArgClass(x: String*) extends WitnessInterface { + + @JSExportTopLevel("ExportedVarArgClass") + def this(x: Int, y: String) = this(s"Number: <$x>", y) + + def witness: String = x.mkString("|") +} + +@JSExportTopLevel("ExportedDefaultArgClass") +class ExportedDefaultArgClass(x: Int, y: Int, z: Int) extends WitnessInterface { + + @JSExportTopLevel("ExportedDefaultArgClass") + def this(x: Int, y: Int = 5) = this(x, y, 100) + + def witness: Int = x + y + z +} + +object ExportHolder { + @JSExportTopLevel("NestedExportedClass") + class ExportedClass + + @JSExportTopLevel("NestedExportedObject") + object ExportedObject + + @JSExportTopLevel("NestedSJSDefinedExportedClass") + class SJSDefinedExportedClass extends js.Object +} + +object TopLevelExports { + @JSExportTopLevel("TopLevelExport_basic") + def basic(): Int = 1 + + @JSExportTopLevel("TopLevelExport_overload") + def overload(x: String): String = "Hello " + x + + @JSExportTopLevel("TopLevelExport_overload") + def overload(x: Int, y: Int*): Int = x + y.sum + + @JSExportTopLevel("TopLevelExport_defaultParams") + def defaultParams(x: Int, y: Int = 1): Int = x + y + + var myVar: Int = _ + + @JSExportTopLevel("TopLevelExport_set") + def setMyVar(x: Int): Unit = myVar = x + + object Nested { + var myVar: Int = _ + + @JSExportTopLevel("TopLevelExport_setNested") + def setMyVar(x: Int): Unit = myVar = x + } + + @JSExportTopLevel("__topLevelExportWithDoubleUnderscore") + val topLevelExportWithDoubleUnderscore: Boolean = true +} + +/* This object is only reachable via the top level export to make sure the + * analyzer behaves correctly. + */ +object TopLevelExportsReachability { + private val name = "World" + + @JSExportTopLevel("TopLevelExport_reachability") + def basic(): String = "Hello " + name +} + +object TopLevelFieldExports { + @JSExportTopLevel("TopLevelExport_basicVal") + val basicVal: Int = 5 + + @JSExportTopLevel("TopLevelExport_basicVar") + var basicVar: String = "hello" + + @JSExportTopLevel("TopLevelExport_valExportedTwice1") + @JSExportTopLevel("TopLevelExport_valExportedTwice2") + val valExportedTwice: Int = 5 + + @JSExportTopLevel("TopLevelExport_varExportedTwice1") + @JSExportTopLevel("TopLevelExport_varExportedTwice2") + var varExportedTwice: String = "hello" + + @JSExportTopLevel("TopLevelExport_uninitializedVarInt") + var uninitializedVarInt: Int = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarLong") + var uninitializedVarLong: Long = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarString") + var uninitializedVarString: String = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarChar") + var uninitializedVarChar: Char = _ + + // the export is only to make the field IR-static + @JSExportTopLevel("TopLevelExport_irrelevant") + @(inline @meta.getter @meta.setter) + var inlineVar: String = "hello" +} + +/* This object and its static initializer are only reachable via the top-level + * export of its field, to make sure the analyzer and the static initiliazer + * behave correctly. + */ +object TopLevelFieldExportsReachability { + private val name = "World" + + @JSExportTopLevel("TopLevelExport_fieldreachability") + val greeting = "Hello " + name +} diff --git a/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/WasmExportLoopback.scala b/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/WasmExportLoopback.scala new file mode 100644 index 00000000..9af71246 --- /dev/null +++ b/scalajs-test-suite/src/test/scala/org/scalajs/testsuite/jsinterop/WasmExportLoopback.scala @@ -0,0 +1,23 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.jsinterop + +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +import scala.concurrent.Future + +object ExportLoopback { + val exportsNamespace: Future[js.Dynamic] = + js.`import`[js.Dynamic]("./main.mjs").toFuture +} diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala index dd280440..a725f81f 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala @@ -68,14 +68,35 @@ class ClassEmitter(coreSpec: CoreSpec) { } } + /** Generates code for a top-level export. + * + * The strategy for top-level exports is as follows: + * + * - the JS code declares a non-initialized `let` for every top-level export, and exports it + * from the module with an ECMAScript `export` + * - the JS code provides a setter function that we import into a Wasm, which allows to set the + * value of that `let` + * - the Wasm code "publishes" every update to top-level exports to the JS code via this + * setter; this happens once in the `start` function for every kind of top-level export (see + * `Emitter.genStartFunction`), and in addition upon each reassignment of a top-level + * exported field (see `FunctionEmitter.genAssign`). + * + * This method declares the import of the setter on the Wasm side, for all kinds of top-level + * exports. In addition, for exported *methods*, it generates the implementation of the method as + * a Wasm function. + * + * The JS code is generated by `Emitter.buildJSFileContent`. Note that for fields, the JS `let`s + * are only "mirrors" of the state. The source of truth for the state remains in the Wasm Global + * for the static field. This is fine because, by spec of ECMAScript modules, JavaScript code + * that *uses* the export cannot mutate it; it can only read it. + */ def transformTopLevelExport( topLevelExport: LinkedTopLevelExport )(implicit ctx: WasmContext): Unit = { + genTopLevelExportSetter(topLevelExport.exportName) topLevelExport.tree match { - case d: TopLevelJSClassExportDef => genDelayedTopLevelExport(d.exportName) - case d: TopLevelModuleExportDef => genDelayedTopLevelExport(d.exportName) - case d: TopLevelMethodExportDef => transformTopLevelMethodExportDef(d) - case d: TopLevelFieldExportDef => transformTopLevelFieldExportDef(d) + case d: TopLevelMethodExportDef => transformTopLevelMethodExportDef(d) + case _ => () } } @@ -1089,6 +1110,21 @@ class ClassEmitter(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** Generates the function import for a top-level export setter. */ + private def genTopLevelExportSetter(exportedName: String)(implicit ctx: WasmContext): Unit = { + val functionName = genFunctionName.forTopLevelExportSetter(exportedName) + val functionSig = wamod.FunctionSignature(List(watpe.RefType.anyref), Nil) + val functionType = ctx.moduleBuilder.signatureToTypeName(functionSig) + + ctx.moduleBuilder.addImport( + wamod.Import( + "__scalaJSExportSetters", + exportedName, + wamod.ImportDesc.Func(functionName, functionType) + ) + ) + } + private def transformTopLevelMethodExportDef( exportDef: TopLevelMethodExportDef )(implicit ctx: WasmContext): Unit = { @@ -1108,51 +1144,6 @@ class ClassEmitter(coreSpec: CoreSpec) { method.body, resultType = AnyType ) - - if (method.restParam.isEmpty) { - ctx.addExport(wamod.Export.Function(exportedName, functionName)) - } else { - /* We cannot directly export the function. We will create a closure - * wrapper in the start function and export that instead. - */ - genDelayedTopLevelExport(exportedName) - } - } - - private def transformTopLevelFieldExportDef( - exportDef: TopLevelFieldExportDef - )(implicit ctx: WasmContext): Unit = { - val exprt = wamod.Export.Global( - exportDef.exportName, - genGlobalName.forStaticField(exportDef.field.name) - ) - ctx.addExport(exprt) - } - - /** Generates a delayed top-level export global, to be initialized in the `start` function. - * - * Some top-level exports need to be initialized by run-time code because they need to call - * initializing functions: - * - * - methods with a `...rest` need to be initialized with the `closureRestNoArg` helper. - * - JS classes need to be initialized with their `loadJSClass` helper. - * - JS modules need to be initialized with their `loadModule` helper. - * - * For all of those, we use `genDelayedTopLevelExport` to generate a Wasm global initialized with - * `null` and to export it. We actually initialize the global in the `start` function (see - * `genStartFunction()` in `WasmContext`). - */ - private def genDelayedTopLevelExport(exportedName: String)(implicit ctx: WasmContext): Unit = { - val globalName = genGlobalName.forTopLevelExport(exportedName) - ctx.addGlobal( - wamod.Global( - globalName, - watpe.RefType.anyref, - wamod.Expr(List(wa.REF_NULL(watpe.HeapType.None))), - isMutable = true - ) - ) - ctx.addExport(wamod.Export.Global(exportedName, globalName)) } private def genFunction( diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala index 9331683e..5fc33dd2 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala @@ -377,8 +377,10 @@ object CoreWasmLib { List(RefType.func, anyref, Int32), List(RefType.any) ) + + addHelperImport(genFunctionName.makeExportedDef, List(RefType.func), List(RefType.any)) addHelperImport( - genFunctionName.closureRestNoData, + genFunctionName.makeExportedDefRest, List(RefType.func, Int32), List(RefType.any) ) diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala index 72add60a..8bc3067d 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala @@ -182,25 +182,31 @@ final class Emitter(config: Emitter.Config) { // Initialize the top-level exports that require it for (tle <- topLevelExportDefs) { + // Load the (initial) exported value on the stack tle.tree match { case TopLevelJSClassExportDef(_, exportName) => instrs += wa.CALL(genFunctionName.loadJSClass(tle.owningClass)) - instrs += wa.GLOBAL_SET(genGlobalName.forTopLevelExport(tle.exportName)) case TopLevelModuleExportDef(_, exportName) => instrs += wa.CALL(genFunctionName.loadModule(tle.owningClass)) - instrs += wa.GLOBAL_SET(genGlobalName.forTopLevelExport(tle.exportName)) case TopLevelMethodExportDef(_, methodDef) => - // We only need initialization if there is a restParam + instrs += ctx.refFuncWithDeclaration(genFunctionName.forExport(tle.exportName)) if (methodDef.restParam.isDefined) { - instrs += ctx.refFuncWithDeclaration(genFunctionName.forExport(tle.exportName)) instrs += wa.I32_CONST(methodDef.args.size) - instrs += wa.CALL(genFunctionName.closureRestNoData) - instrs += wa.GLOBAL_SET(genGlobalName.forTopLevelExport(tle.exportName)) + instrs += wa.CALL(genFunctionName.makeExportedDefRest) + } else { + instrs += wa.CALL(genFunctionName.makeExportedDef) } - case TopLevelFieldExportDef(_, _, _) => - // Nothing to do - () + case TopLevelFieldExportDef(_, _, fieldIdent) => + /* Usually redundant, but necessary if the static field is never + * explicitly set and keeps its default (zero) value instead. In that + * case this initial call is required to publish that zero value (as + * opposed to the default `undefined` value of the JS `let`). + */ + instrs += wa.GLOBAL_GET(genGlobalName.forStaticField(fieldIdent.name)) } + + // Call the export setter + instrs += wa.CALL(genFunctionName.forTopLevelExportSetter(tle.exportName)) } // Emit the module initializers @@ -282,24 +288,27 @@ final class Emitter(config: Emitter.Config) { (moduleImport, item) }).unzip - /* TODO This is not correct for exported *vars*, since they won't receive - * updates from mutations after loading. - */ - val reExportStats = for { + val (exportDecls, exportSetters) = (for { exportName <- module.topLevelExports.map(_.exportName) } yield { - s"export let $exportName = __exports.$exportName;" - } + val identName = s"exported$exportName" + val decl = s"let $identName;\nexport { $identName as $exportName };" + val setter = s" $exportName: (x) => $identName = x," + (decl, setter) + }).unzip s""" |${moduleImports.mkString("\n")} | |import { load as __load } from './${config.loaderModuleName}'; - |const __exports = await __load('./${wasmFileName}', { + | + |${exportDecls.mkString("\n")} + | + |await __load('./${wasmFileName}', { |${importedModulesItems.mkString("\n")} + |}, { + |${exportSetters.mkString("\n")} |}); - | - |${reExportStats.mkString("\n")} """.stripMargin.trim() + "\n" } } diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index 9614dedb..482a0c43 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -460,8 +460,19 @@ private class FunctionEmitter private ( } case sel: SelectStatic => + val fieldName = sel.field.name + val globalName = genGlobalName.forStaticField(fieldName) + genTree(t.rhs, sel.tpe) - instrs += wa.GLOBAL_SET(genGlobalName.forStaticField(sel.field.name)) + instrs += wa.GLOBAL_SET(globalName) + + // Update top-level export mirrors + val classInfo = ctx.getClassInfo(fieldName.className) + val mirrors = classInfo.staticFieldMirrors.getOrElse(fieldName, Nil) + for (exportedName <- mirrors) { + instrs += wa.GLOBAL_GET(globalName) + instrs += wa.CALL(genFunctionName.forTopLevelExportSetter(exportedName)) + } case sel: ArraySelect => genTreeAuto(sel.array) diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala index 2b0c80b5..989c72c0 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala @@ -119,7 +119,10 @@ const scalaJSHelpers = { closureThis: (f, data) => function(...args) { return f(data, this, ...args); }, closureRest: (f, data, n) => ((...args) => f(data, ...args.slice(0, n), args.slice(n))), closureThisRest: (f, data, n) => function(...args) { return f(data, this, ...args.slice(0, n), args.slice(n)); }, - closureRestNoData: (f, n) => ((...args) => f(...args.slice(0, n), args.slice(n))), + + // Top-level exported defs -- they must be `function`s but have no actual `this` nor `data` + makeExportedDef: (f) => function(...args) { return f(...args); }, + makeExportedDefRest: (f, n) => function(...args) { return f(...args.slice(0, n), args.slice(n)); }, // Strings emptyString: "", @@ -305,11 +308,12 @@ const scalaJSHelpers = { }, } -export async function load(wasmFileURL, importedModules) { +export async function load(wasmFileURL, importedModules, exportSetters) { const myScalaJSHelpers = { ...scalaJSHelpers, idHashCodeMap: new WeakMap() }; const importsObj = { "__scalaJSHelpers": myScalaJSHelpers, "__scalaJSImports": importedModules, + "__scalaJSExportSetters": exportSetters, }; const resolvedURL = new URL(wasmFileURL, import.meta.url); var wasmModulePromise; @@ -323,24 +327,7 @@ export async function load(wasmFileURL, importedModules) { } else { wasmModulePromise = WebAssembly.instantiateStreaming(fetch(resolvedURL), importsObj); } - const wasmModule = await wasmModulePromise; - const exports = wasmModule.instance.exports; - - const userExports = Object.create(null); - for (const exportName of Object.getOwnPropertyNames(exports)) { - const exportValue = exports[exportName]; - if (exportValue instanceof WebAssembly.Global) { - Object.defineProperty(userExports, exportName, { - configurable: true, - enumerable: true, - get: () => exportValue.value, - }); - } else { - userExports[exportName] = exportValue; - } - } - Object.freeze(userExports); - return userExports; + await wasmModulePromise; } """ } diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala index 2fc181e0..b759925b 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala @@ -14,8 +14,10 @@ object Preprocessor { def preprocess(classes: List[LinkedClass], tles: List[LinkedTopLevelExport])(implicit ctx: WasmContext ): Unit = { + val staticFieldMirrors = computeStaticFieldMirrors(tles) + for (clazz <- classes) - preprocess(clazz) + preprocess(clazz, staticFieldMirrors.getOrElse(clazz.className, Map.empty)) val collector = new AbstractMethodCallCollector(ctx) for (clazz <- classes) @@ -29,7 +31,28 @@ object Preprocessor { ctx.assignBuckets(classes) } - private def preprocess(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + private def computeStaticFieldMirrors( + tles: List[LinkedTopLevelExport] + ): Map[ClassName, Map[FieldName, List[String]]] = { + var result = Map.empty[ClassName, Map[FieldName, List[String]]] + for (tle <- tles) { + tle.tree match { + case TopLevelFieldExportDef(_, exportName, FieldIdent(fieldName)) => + val className = tle.owningClass + val mirrors = result.getOrElse(className, Map.empty) + val newExportNames = exportName :: mirrors.getOrElse(fieldName, Nil) + val newMirrors = mirrors.updated(fieldName, newExportNames) + result = result.updated(className, newMirrors) + + case _ => + } + } + result + } + + private def preprocess(clazz: LinkedClass, staticFieldMirrors: Map[FieldName, List[String]])( + implicit ctx: WasmContext + ): Unit = { val kind = clazz.kind val allFieldDefs: List[FieldDef] = @@ -96,6 +119,7 @@ object Preprocessor { hasRuntimeTypeInfo, clazz.jsNativeLoadSpec, clazz.jsNativeMembers.map(m => m.name.name -> m.jsNativeLoadSpec).toMap, + staticFieldMirrors, _itableIdx = -1 ) ) diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala index 422c7355..57b44b5f 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala @@ -33,9 +33,6 @@ object VarGen { def forStaticField(fieldName: IRFieldName): GlobalName = GlobalName(s"static.${fieldName.nameString}") - def forTopLevelExport(exportName: String): GlobalName = - GlobalName(s"export.$exportName") - def forJSPrivateField(fieldName: IRFieldName): GlobalName = GlobalName(s"jspfield.${fieldName.nameString}") @@ -103,6 +100,8 @@ object VarGen { def forExport(exportedName: String): FunctionName = make("export", exportedName) + def forTopLevelExportSetter(exportedName: String): FunctionName = + make("setexport", exportedName) def loadModule(clazz: ClassName): FunctionName = make("loadModule", clazz.nameString) @@ -153,7 +152,9 @@ object VarGen { val closureThis = make("closureThis") val closureRest = make("closureRest") val closureThisRest = make("closureThisRest") - val closureRestNoData = make("closureRestNoData") + + val makeExportedDef = make("makeExportedDef") + val makeExportedDefRest = make("makeExportedDefRest") val stringLength = make("stringLength") val stringCharAt = make("stringCharAt") diff --git a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala index 3c5cf66b..768a0f87 100644 --- a/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala +++ b/wasm/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala @@ -410,6 +410,7 @@ object WasmContext { val hasRuntimeTypeInfo: Boolean, val jsNativeLoadSpec: Option[JSNativeLoadSpec], val jsNativeMembers: Map[MethodName, JSNativeLoadSpec], + val staticFieldMirrors: Map[FieldName, List[String]], private var _itableIdx: Int ) { private val fieldIdxByName: Map[FieldName, Int] =