Skip to content

Commit

Permalink
Scala.js: Implement the PrepJSInterop phase, minus exports handling.
Browse files Browse the repository at this point in the history
The `PrepJSInterop` phase is responsible for:

* Performing all kinds of Scala.js-specific compile-time checks,
  and emitting the appropriate compile errors.
* Perform some transformations that are necessary for JavaScript
  interop, notably generating exports forwarders.

This commit ports all the functionality of `PrepJSInterop` from
Scala 2, except the following:

* Handling of `scala.Enumeration`s: it is unclear whether we still
  want to support that in the core, or if it should be handled by
  an optional compiler plugin in the future.
* Exports: they will be done later.
* Warnings about duplicate fields in `js.Dynamic.literal`: mostly
  because they are non-essential.

The test cases are ported from the Scala.js compiler tests.
  • Loading branch information
sjrd committed Sep 14, 2020
1 parent c9b8e7a commit 9bcba44
Show file tree
Hide file tree
Showing 104 changed files with 5,185 additions and 88 deletions.
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)
}
}

/** 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.|")
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

0 comments on commit 9bcba44

Please sign in to comment.