Skip to content

Scala.js: Implement the PrepJSInterop phase, minus exports handling. #9725

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 2 commits into from
Sep 14, 2020
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
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
126 changes: 111 additions & 15 deletions compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import org.scalajs.ir.OriginalName
import org.scalajs.ir.OriginalName.NoOriginalName
import org.scalajs.ir.Trees.OptimizerHints

import dotty.tools.dotc.transform.sjs.JSSymUtils._

import JSEncoding._
import JSInterop._
import ScopedVar.withScopedVars
Expand All @@ -60,6 +62,7 @@ class JSCodeGen()(using genCtx: Context) {
import JSCodeGen._
import tpd._

private val sjsPlatform = dotty.tools.dotc.config.SJSPlatform.sjsPlatform
private val jsdefn = JSDefinitions.jsdefn
private val primitives = new JSPrimitives(genCtx)

Expand Down Expand Up @@ -461,14 +464,7 @@ class JSCodeGen()(using genCtx: Context) {
val superClass =
if (sym.is(Trait)) None
else Some(encodeClassNameIdent(sym.superClass))
val jsNativeLoadSpec = {
if (sym.is(Trait)) None
else if (sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) None
else {
val path = fullJSNameOf(sym).split('.').toList
Some(js.JSNativeLoadSpec.Global(path.head, path.tail))
}
}
val jsNativeLoadSpec = computeJSNativeLoadSpecOfClass(sym)

js.ClassDef(
classIdent,
Expand Down Expand Up @@ -1008,6 +1004,30 @@ class JSCodeGen()(using genCtx: Context) {
result
}

private def genExpr(name: JSName)(implicit pos: SourcePosition): js.Tree = name match {
case JSName.Literal(name) => js.StringLiteral(name)
case JSName.Computed(sym) => genComputedJSName(sym)
}

private def genComputedJSName(sym: Symbol)(implicit pos: SourcePosition): js.Tree = {
/* By construction (i.e. restriction in PrepJSInterop), we know that sym
* must be a static method.
* Therefore, at this point, we can invoke it by loading its owner and
* calling it.
*/
def moduleOrGlobalScope = genLoadModuleOrGlobalScope(sym.owner)
def module = genLoadModule(sym.owner)

if (sym.owner.isJSType) {
if (!sym.owner.isNonNativeJSClass || sym.isJSExposed)
genApplyJSMethodGeneric(sym, moduleOrGlobalScope, args = Nil, isStat = false)
else
genApplyJSClassMethod(module, sym, arguments = Nil)
} else {
genApplyMethod(module, sym, arguments = Nil)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd that the distinction of receiver types needs to be made here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also done in scala-js/scala-js:
https://github.com/scala-js/scala-js/blob/730931a3a11b351ea850a5c6107cfbd332c52a9b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala#L6171-L6188

That is necessary because the owner SomeObject referred to in an @JSName(SomeObject.aSymbolVal) can be a Scala object, a native JS object or a non-native JS object.

}

/** Gen JS code for a tree in expression position (in the IR) or the
* global scope.
*/
Expand Down Expand Up @@ -2096,7 +2116,7 @@ class JSCodeGen()(using genCtx: Context) {
genApplyStatic(sym, genActualArgs(sym, args))
} else if (isJSType(sym.owner)) {
//if (!isScalaJSDefinedJSClass(sym.owner) || isExposed(sym))
genApplyJSMethodGeneric(tree, sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat)
genApplyJSMethodGeneric(sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat)(tree.sourcePos)
/*else
genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))*/
} else {
Expand All @@ -2115,19 +2135,17 @@ class JSCodeGen()(using genCtx: Context) {
* - Getters and parameterless methods are translated as `JSBracketSelect`
* - Setters are translated to `Assign` to `JSBracketSelect`
*/
private def genApplyJSMethodGeneric(tree: Tree, sym: Symbol,
private def genApplyJSMethodGeneric(sym: Symbol,
receiver: MaybeGlobalScope, args: List[js.TreeOrJSSpread], isStat: Boolean,
jsSuperClassValue: Option[js.Tree] = None)(
implicit pos: Position): js.Tree = {

implicit val pos: SourcePosition = tree.sourcePos
implicit pos: SourcePosition): js.Tree = {

def noSpread = !args.exists(_.isInstanceOf[js.JSSpread])
val argc = args.size // meaningful only for methods that don't have varargs

def requireNotSuper(): Unit = {
if (jsSuperClassValue.isDefined)
report.error("Illegal super call in Scala.js-defined JS class", tree.sourcePos)
report.error("Illegal super call in Scala.js-defined JS class", pos)
}

def requireNotSpread(arg: js.TreeOrJSSpread): js.Tree =
Expand Down Expand Up @@ -2156,7 +2174,7 @@ class JSCodeGen()(using genCtx: Context) {
js.JSFunctionApply(ruleOutGlobalScope(receiver), args)

case _ =>
def jsFunName = js.StringLiteral(jsNameOf(sym))
def jsFunName = genExpr(jsNameOf(sym))

def genSuperReference(propName: js.Tree): js.Tree = {
jsSuperClassValue.fold[js.Tree] {
Expand Down Expand Up @@ -3479,6 +3497,84 @@ class JSCodeGen()(using genCtx: Context) {
}
}

private def computeJSNativeLoadSpecOfClass(sym: Symbol): Option[js.JSNativeLoadSpec] = {
if (sym.is(Trait) || sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) {
None
} else {
atPhase(picklerPhase.next) {
if (sym.owner.isStaticOwner)
Some(computeJSNativeLoadSpecOfInPhase(sym))
else
None
}
}
}

private def computeJSNativeLoadSpecOfInPhase(sym: Symbol)(using Context): js.JSNativeLoadSpec = {
import js.JSNativeLoadSpec._

val symOwner = sym.owner

// Marks a code path as unexpected because it should have been reported as an error in `PrepJSInterop`.
def unexpected(msg: String): Nothing =
throw new FatalError(i"$msg for ${sym.fullName} at ${sym.srcPos}")

if (symOwner.hasAnnotation(jsdefn.JSNativeAnnot)) {
val jsName = sym.jsName match {
case JSName.Literal(jsName) => jsName
case JSName.Computed(_) => unexpected("could not read the simple JS name as a string literal")
}

if (symOwner.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) {
Global(jsName, Nil)
} else {
val ownerLoadSpec = computeJSNativeLoadSpecOfInPhase(symOwner)
ownerLoadSpec match {
case Global(globalRef, path) =>
Global(globalRef, path :+ jsName)
case Import(module, path) =>
Import(module, path :+ jsName)
case ImportWithGlobalFallback(Import(module, modulePath), Global(globalRef, globalPath)) =>
ImportWithGlobalFallback(
Import(module, modulePath :+ jsName),
Global(globalRef, globalPath :+ jsName))
}
}
} else {
def parsePath(pathName: String): List[String] =
pathName.split('.').toList

def parseGlobalPath(pathName: String): Global = {
val globalRef :: path = parsePath(pathName)
Global(globalRef, path)
}

val annot = sym.annotations.find { annot =>
annot.symbol == jsdefn.JSGlobalAnnot || annot.symbol == jsdefn.JSImportAnnot
}.getOrElse {
unexpected("could not find the JS native load spec annotation")
}

if (annot.symbol == jsdefn.JSGlobalAnnot) {
val pathName = annot.argumentConstantString(0).getOrElse {
sym.defaultJSName
}
parseGlobalPath(pathName)
} else { // annot.symbol == jsdefn.JSImportAnnot
val module = annot.argumentConstantString(0).getOrElse {
unexpected("could not read the module argument as a string literal")
}
val path = annot.argumentConstantString(1).fold[List[String]](Nil)(parsePath)
val importSpec = Import(module, path)
annot.argumentConstantString(2).fold[js.JSNativeLoadSpec] {
importSpec
} { globalPathName =>
ImportWithGlobalFallback(importSpec, parseGlobalPath(globalPathName))
}
}
}
}

private def isMethodStaticInIR(sym: Symbol): Boolean =
sym.is(JavaStatic)

Expand Down
32 changes: 23 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 All @@ -50,6 +52,11 @@ final class JSDefinitions()(using Context) {
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass

@threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need to special case this? I thought dotty supports union types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dotty supports union types, but it can also interoperate with Scala 2 libraries. And in particular with the pseudo | type in scalajs-library, which could itself be referenced by other methods in the std lib or in third-party libs. In Scala 3.0, we'll still need to recognize Scala.js' | type and correctly handle it. In 3.1, when we get rid of the compatibility with Scala 2 binaries, we'll be able to entirely drop scala.scalajs.js.| from the library, and have all usages of | be the true union type of dotty.

Also note that we cannot "reinterpret" scala.scalajs.js.| as a dotty union type during Scala2Unpickler because they do not have the same erasure: scala.scalajs.js.|[A, B] erases to any with a ClassRef("scala.scalajs.js.|"), whereas a true A | B erases to the JVM lub of A and B, with a corresponding ClassRef.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that we cannot "reinterpret" scala.scalajs.js.| as a dotty union type during Scala2Unpickler because they do not have the same erasure

The same is true for Scala 2 intersection types versus Dotty &, I have a wip branch which erases those differently, so we could extend that mechanism to handle unions too if needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean we could reinterpret scala.scalajs.js.| as a special brand of union type that somehow erases differently? That would be awesome!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that should be doable.

def PseudoUnionModule(using Context) = PseudoUnionModuleRef.symbol
@threadUnsafe lazy val PseudoUnion_fromTypeConstructorR = PseudoUnionModule.requiredMethodRef("fromTypeConstructor")
def PseudoUnion_fromTypeConstructor(using Context) = PseudoUnion_fromTypeConstructorR.symbol

@threadUnsafe lazy val JSArrayType: TypeRef = requiredClassRef("scala.scalajs.js.Array")
def JSArrayClass(using Context) = JSArrayType.symbol.asClass

Expand All @@ -63,6 +70,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 +84,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.JSSymUtils._

/** Management of the interoperability with JavaScript. */
/** Management of the interoperability with JavaScript.
*
* This object only contains forwarders for extension methods in
* `transform.sjs.JSSymUtils`. 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
Loading