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 9, 2020
1 parent b0f2868 commit 8179687
Show file tree
Hide file tree
Showing 102 changed files with 5,164 additions and 87 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
48 changes: 33 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.JSInteropUtils._

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 @@ -426,14 +429,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 = sjsPlatform.perRunInfo.jsNativeLoadSpecOfOption(sym)

js.ClassDef(
classIdent,
Expand Down Expand Up @@ -954,6 +950,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.isExposed)
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 @@ -2042,7 +2062,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 @@ -2061,19 +2081,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 @@ -2102,7 +2120,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
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
3 changes: 3 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,9 @@ class JavaPlatform extends Platform {
(sym derivesFrom BoxedBooleanClass)
}

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

def newClassLoader(bin: AbstractFile)(using Context): SymbolLoader =
new ClassfileLoader(bin)
}
4 changes: 3 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,9 @@ abstract class Platform {
/** The various ways a boxed primitive might materialize at runtime. */
def isMaybeBoxed(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 +47,3 @@ abstract class Platform {
case _ => false
}
}

32 changes: 32 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,27 @@ class SJSPlatform()(using Context) extends JavaPlatform {
defn.isFunctionClass(cls)
|| jsDefinitions.isJSFunctionClass(cls)
|| jsDefinitions.isJSThisFunctionClass(cls)

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)
}
}
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
4 changes: 4 additions & 0 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
Loading

0 comments on commit 8179687

Please sign in to comment.