Skip to content

Scala.js: Various JS interop fixes #9763

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

Closed
wants to merge 13 commits into from
Closed
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
1 change: 1 addition & 0 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ test_script:
# - cmd: sbt test
# - cmd: sbt dotty-bootstrapped/test
- cmd: sbt sjsJUnitTests/test
- cmd: sbt sjsCompilerTests/test
204 changes: 160 additions & 44 deletions compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Large diffs are not rendered by default.

27 changes: 18 additions & 9 deletions compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class JSDefinitions()(using Context) {
def JSPackage_constructorOf(using Context) = JSPackage_constructorOfR.symbol
@threadUnsafe lazy val JSPackage_nativeR = ScalaJSJSPackageClass.requiredMethodRef("native")
def JSPackage_native(using Context) = JSPackage_nativeR.symbol
@threadUnsafe lazy val JSPackage_undefinedR = ScalaJSJSPackageClass.requiredMethodRef("undefined")
def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol

@threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native")
def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass
Expand Down Expand Up @@ -63,6 +65,10 @@ final class JSDefinitions()(using Context) {
@threadUnsafe lazy val JavaScriptExceptionType: TypeRef = requiredClassRef("scala.scalajs.js.JavaScriptException")
def JavaScriptExceptionClass(using Context) = JavaScriptExceptionType.symbol.asClass

@threadUnsafe lazy val JSGlobalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSGlobal")
def JSGlobalAnnot(using Context) = JSGlobalAnnotType.symbol.asClass
@threadUnsafe lazy val JSImportAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSImport")
def JSImportAnnot(using Context) = JSImportAnnotType.symbol.asClass
@threadUnsafe lazy val JSGlobalScopeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSGlobalScope")
def JSGlobalScopeAnnot(using Context) = JSGlobalScopeAnnotType.symbol.asClass
@threadUnsafe lazy val JSNameAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSName")
Expand All @@ -73,21 +79,24 @@ final class JSDefinitions()(using Context) {
def JSBracketAccessAnnot(using Context) = JSBracketAccessAnnotType.symbol.asClass
@threadUnsafe lazy val JSBracketCallAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSBracketCall")
def JSBracketCallAnnot(using Context) = JSBracketCallAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportTopLevelAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportTopLevel")
def JSExportTopLevelAnnot(using Context) = JSExportTopLevelAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExport")
def JSExportAnnot(using Context) = JSExportAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportDescendentObjectsAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportDescendentObjects")
def JSExportDescendentObjectsAnnot(using Context) = JSExportDescendentObjectsAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportDescendentClassesAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportDescendentClasses")
def JSExportDescendentClassesAnnot(using Context) = JSExportDescendentClassesAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportStaticAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportStatic")
def JSExportStaticAnnot(using Context) = JSExportStaticAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportAllAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportAll")
def JSExportAllAnnot(using Context) = JSExportAllAnnotType.symbol.asClass
@threadUnsafe lazy val JSExportNamedAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportNamed")
def JSExportNamedAnnot(using Context) = JSExportNamedAnnotType.symbol.asClass
@threadUnsafe lazy val RawJSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.RawJSType")
def RawJSTypeAnnot(using Context) = RawJSTypeAnnotType.symbol.asClass
@threadUnsafe lazy val ExposedJSMemberAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.ExposedJSMember")
@threadUnsafe lazy val JSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSType")
def JSTypeAnnot(using Context) = JSTypeAnnotType.symbol.asClass
@threadUnsafe lazy val JSOptionalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSOptional")
def JSOptionalAnnot(using Context) = JSOptionalAnnotType.symbol.asClass
@threadUnsafe lazy val ExposedJSMemberAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.ExposedJSMember")
def ExposedJSMemberAnnot(using Context) = ExposedJSMemberAnnotType.symbol.asClass

@threadUnsafe lazy val JSImportNamespaceModuleRef = requiredModuleRef("scala.scalajs.js.annotation.JSImport.Namespace")
def JSImportNamespaceModule(using Context) = JSImportNamespaceModuleRef.symbol

@threadUnsafe lazy val JSAnyModuleRef = requiredModuleRef("scala.scalajs.js.Any")
def JSAnyModule(using Context) = JSAnyModuleRef.symbol
@threadUnsafe lazy val JSAny_fromFunctionR = (0 to 22).map(n => JSAnyModule.requiredMethodRef("fromFunction" + n)).toArray
Expand Down
77 changes: 19 additions & 58 deletions compiler/src/dotty/tools/backend/sjs/JSInterop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,33 @@ import Symbols._
import NameOps._
import StdNames._
import Phases._
import NameKinds.DefaultGetterName

import JSDefinitions._
import dotty.tools.dotc.transform.sjs.JSInteropUtils._

/** Management of the interoperability with JavaScript. */
/** Management of the interoperability with JavaScript.
*
* This object only contains forwarders for extension methods in
* `transform.sjs.JSInteropUtils`. They are kept to minimize changes in
* `JSCodeGen` in the short term, but it will eventually be removed.
*/
object JSInterop {

/** Is this symbol a JavaScript type? */
def isJSType(sym: Symbol)(using Context): Boolean = {
atPhase(erasurePhase) {
sym.derivesFrom(jsdefn.JSAnyClass) || sym == jsdefn.PseudoUnionClass
}
}
def isJSType(sym: Symbol)(using Context): Boolean =
sym.isJSType

/** Is this symbol a Scala.js-defined JS class, i.e., a non-native JS class? */
def isScalaJSDefinedJSClass(sym: Symbol)(using Context): Boolean =
isJSType(sym) && !sym.hasAnnotation(jsdefn.JSNativeAnnot)
sym.isNonNativeJSClass

/** Should this symbol be translated into a JS getter?
*
* This is true for any parameterless method, i.e., defined without `()`.
* Unlike `SymDenotations.isGetter`, it applies to user-defined methods as
* much as *accessor* methods created for `val`s and `var`s.
*/
def isJSGetter(sym: Symbol)(using Context): Boolean = {
sym.info.firstParamTypes.isEmpty && atPhase(erasurePhase) {
sym.info.isParameterless
}
}
def isJSGetter(sym: Symbol)(using Context): Boolean =
sym.isJSGetter

/** Should this symbol be translated into a JS setter?
*
Expand All @@ -44,74 +42,37 @@ object JSInterop {
* much as *accessor* methods created for `var`s.
*/
def isJSSetter(sym: Symbol)(using Context): Boolean =
sym.name.isSetterName && sym.is(Method)
sym.isJSSetter

/** Should this symbol be translated into a JS bracket access?
*
* This is true for methods annotated with `@JSBracketAccess`.
*/
def isJSBracketAccess(sym: Symbol)(using Context): Boolean =
sym.hasAnnotation(jsdefn.JSBracketAccessAnnot)
sym.isJSBracketAccess

/** Should this symbol be translated into a JS bracket call?
*
* This is true for methods annotated with `@JSBracketCall`.
*/
def isJSBracketCall(sym: Symbol)(using Context): Boolean =
sym.hasAnnotation(jsdefn.JSBracketCallAnnot)
sym.isJSBracketCall

/** Is this symbol a default param accessor for a JS method?
*
* For default param accessors of *constructors*, we need to test whether
* the companion *class* of the owner is a JS type; not whether the owner
* is a JS type.
*/
def isJSDefaultParam(sym: Symbol)(using Context): Boolean = {
sym.name.is(DefaultGetterName) && {
val owner = sym.owner
if (owner.is(ModuleClass)) {
val isConstructor = sym.name match {
case DefaultGetterName(methName, _) => methName == nme.CONSTRUCTOR
case _ => false
}
if (isConstructor)
isJSType(owner.linkedClass)
else
isJSType(owner)
} else {
isJSType(owner)
}
}
}
def isJSDefaultParam(sym: Symbol)(using Context): Boolean =
sym.isJSDefaultParam

/** Gets the unqualified JS name of a symbol.
*
* If it is not explicitly specified with an `@JSName` annotation, the
* JS name is inferred from the Scala name.
*/
def jsNameOf(sym: Symbol)(using Context): String = {
sym.getAnnotation(jsdefn.JSNameAnnot).flatMap(_.argumentConstant(0)).fold {
val base = sym.name.unexpandedName.decode.toString.stripSuffix("_=")
if (sym.is(ModuleClass)) base.stripSuffix("$")
else if (!sym.is(Method)) base.stripSuffix(" ")
else base
} { constant =>
constant.stringValue
}
}

/** Gets the fully qualified JS name of a static class of module Symbol.
*
* This is the JS name of the symbol qualified by the fully qualified JS
* name of its original owner if the latter is a native JS object.
*/
def fullJSNameOf(sym: Symbol)(using Context): String = {
assert(sym.isClass, s"fullJSNameOf called for non-class symbol $sym")
sym.getAnnotation(jsdefn.JSFullNameAnnot).flatMap(_.argumentConstant(0)).fold {
jsNameOf(sym)
} { constant =>
constant.stringValue
}
}
def jsNameOf(sym: Symbol)(using Context): JSName =
sym.jsName

}
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Compiler {
List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks
List(new semanticdb.ExtractSemanticDB) :: // Extract info into .semanticdb files
List(new PostTyper) :: // Additional checks and cleanups after type checking
List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only)
List(new Staging) :: // Check PCP, heal quoted types and expand macros
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ object desugar {
val companionMods = mods
.withFlags((mods.flags & (AccessFlags | Final)).toCommonFlags)
.withMods(Nil)
.withAnnotations(Nil)

var defaultGetters: List[Tree] = Nil

Expand Down
7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/config/JavaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ class JavaPlatform extends Platform {
(sym derivesFrom BoxedBooleanClass)
}

/** Is the given class assured by the platform not to have any initialization code? */
def isAssuredNoInits(sym: ClassSymbol)(using Context): Boolean =
defn.isAssuredNoInits(sym)

def shouldReceiveJavaSerializationMethods(sym: ClassSymbol)(using Context): Boolean =
true

def newClassLoader(bin: AbstractFile)(using Context): SymbolLoader =
new ClassfileLoader(bin)
}
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/config/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ abstract class Platform {
/** The various ways a boxed primitive might materialize at runtime. */
def isMaybeBoxed(sym: ClassSymbol)(using Context): Boolean

/** Is the given class assured by the platform not to have any initialization code? */
def isAssuredNoInits(sym: ClassSymbol)(using Context): Boolean

/** Is the given class symbol eligible for Java serialization-specific methods? */
def shouldReceiveJavaSerializationMethods(sym: ClassSymbol)(using Context): Boolean

/** Create a new class loader to load class file `bin` */
def newClassLoader(bin: AbstractFile)(using Context): SymbolLoader

Expand All @@ -44,4 +50,3 @@ abstract class Platform {
case _ => false
}
}

36 changes: 36 additions & 0 deletions compiler/src/dotty/tools/dotc/config/SJSPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ package dotty.tools.dotc.config
import dotty.tools.dotc.core._
import Contexts._
import Symbols._
import SymDenotations._

import dotty.tools.backend.sjs.JSDefinitions

import org.scalajs.ir.Trees.JSNativeLoadSpec

object SJSPlatform {
/** The `SJSPlatform` for the current context. */
def sjsPlatform(using Context): SJSPlatform =
ctx.platform.asInstanceOf[SJSPlatform]
}

class SJSPlatform()(using Context) extends JavaPlatform {

/** Scala.js-specific definitions. */
Expand All @@ -16,4 +25,31 @@ class SJSPlatform()(using Context) extends JavaPlatform {
defn.isFunctionClass(cls)
|| jsDefinitions.isJSFunctionClass(cls)
|| jsDefinitions.isJSThisFunctionClass(cls)

/** Is the given class assured by the platform not to have any initialization code? */
override def isAssuredNoInits(sym: ClassSymbol)(using Context): Boolean =
super.isAssuredNoInits(sym) || sym.hasAnnotation(jsDefinitions.JSGlobalScopeAnnot)

override def shouldReceiveJavaSerializationMethods(sym: ClassSymbol)(using Context): Boolean =
!sym.isSubClass(jsDefinitions.JSAnyClass)

object perRunInfo {
private val jsNativeLoadSpecs = new MutableSymbolMap[JSNativeLoadSpec]

/** Clears all the info at the beginning of a run. */
def clear(): Unit =
jsNativeLoadSpecs.clear()

/** Stores the JS native load spec of a symbol for the current compilation run. */
def storeJSNativeLoadSpec(sym: Symbol, spec: JSNativeLoadSpec): Unit =
jsNativeLoadSpecs(sym) = spec

/** Gets the JS native load spec of a symbol in the current compilation run. */
def jsNativeLoadSpecOf(sym: Symbol): JSNativeLoadSpec =
jsNativeLoadSpecs(sym)

/** Gets the JS native load spec of a symbol in the current compilation run, if it has one. */
def jsNativeLoadSpecOfOption(sym: Symbol): Option[JSNativeLoadSpec] =
jsNativeLoadSpecs.get(sym)
}
}
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class ScalaSettings extends Settings.SettingGroup {
val pluginsDir: Setting[String] = StringSetting ("-Xpluginsdir", "path", "Path to search for plugin archives.", Defaults.scalaPluginPath)
val pluginOptions: Setting[List[String]] = MultiStringSetting ("-P", "plugin:opt", "Pass an option to a plugin, e.g. -P:<plugin>:<opt>")

/** Scala.js-related settings */
val scalajsGenStaticForwardersForNonTopLevelObjects: Setting[Boolean] = BooleanSetting("-scalajs-genStaticForwardersForNonTopLevelObjects", "Generate static forwarders even for non-top-level objects (Scala.js only)")

/** -X "Advanced" settings
*/
val Xhelp: Setting[Boolean] = BooleanSetting("-X", "Print a synopsis of advanced options.")
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ object Annotations {
if (i < args.length) Some(args(i)) else None
}
def argumentConstant(i: Int)(using Context): Option[Constant] =
for (ConstantType(c) <- argument(i) map (_.tpe)) yield c
for (ConstantType(c) <- argument(i) map (_.tpe.widenTermRefExpr.normalized)) yield c

def argumentConstantString(i: Int)(using Context): Option[String] =
for (Constant(s: String) <- argumentConstant(i)) yield s

/** The tree evaluaton is in progress. */
def isEvaluating: Boolean = false
Expand Down
39 changes: 24 additions & 15 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ object SymDenotations {
final def addAnnotation(annot: Annotation): Unit =
annotations = annot :: myAnnotations

/** Add the given annotation without parameters to the annotations of this denotation */
final def addAnnotation(cls: ClassSymbol)(using Context): Unit =
addAnnotation(Annotation(cls))

/** Remove annotation with given class from this denotation */
final def removeAnnotation(cls: Symbol)(using Context): Unit =
annotations = myAnnotations.filterNot(_ matches cls)
Expand Down Expand Up @@ -491,21 +495,26 @@ object SymDenotations {

/** The name given in an `@alpha` annotation if one is present, `name` otherwise */
final def erasedName(using Context): Name =
getAnnotation(defn.AlphaAnnot) match {
case Some(ann) =>
ann.arguments match {
case Literal(Constant(str: String)) :: Nil =>
if (isType)
if (is(ModuleClass))
str.toTypeName.moduleClassName
else
str.toTypeName
def fromAnnot(ann: Annotation): Name =
ann.arguments match
case Literal(Constant(str: String)) :: Nil =>
if (isType)
if (is(ModuleClass))
str.toTypeName.moduleClassName
else
str.toTermName
case _ => name
}
case _ => name
}
str.toTypeName
else
str.toTermName
case _ => name
getAnnotation(defn.AlphaAnnot) match
case Some(ann) => fromAnnot(ann)
case _ =>
if isAllOf(ModuleClass | Synthetic) then
companionClass.getAnnotation(defn.AlphaAnnot) match
case Some(ann) => fromAnnot(ann)
case _ => name
else
name

// ----- Tests -------------------------------------------------

Expand Down Expand Up @@ -707,7 +716,7 @@ object SymDenotations {
*/
def isNoInitsClass(using Context): Boolean =
isClass &&
(asClass.baseClasses.forall(_.is(NoInits)) || defn.isAssuredNoInits(symbol))
(asClass.baseClasses.forall(_.is(NoInits)) || ctx.platform.isAssuredNoInits(symbol.asClass))

/** Is this a "real" method? A real method is a method which is:
* - not an accessor
Expand Down
Loading