Skip to content

Commit cbb05c8

Browse files
authored
Merge pull request #9725 from dotty-staging/sjs-prep-js-interop
Scala.js: Implement the PrepJSInterop phase, minus exports handling.
2 parents bd05d37 + 9bcba44 commit cbb05c8

File tree

105 files changed

+5190
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+5190
-89
lines changed

.appveyor.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ test_script:
1818
# - cmd: sbt test
1919
# - cmd: sbt dotty-bootstrapped/test
2020
- cmd: sbt sjsJUnitTests/test
21+
- cmd: sbt sjsCompilerTests/test

compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import org.scalajs.ir.OriginalName
3636
import org.scalajs.ir.OriginalName.NoOriginalName
3737
import org.scalajs.ir.Trees.OptimizerHints
3838

39+
import dotty.tools.dotc.transform.sjs.JSSymUtils._
40+
3941
import JSEncoding._
4042
import JSInterop._
4143
import ScopedVar.withScopedVars
@@ -60,6 +62,7 @@ class JSCodeGen()(using genCtx: Context) {
6062
import JSCodeGen._
6163
import tpd._
6264

65+
private val sjsPlatform = dotty.tools.dotc.config.SJSPlatform.sjsPlatform
6366
private val jsdefn = JSDefinitions.jsdefn
6467
private val primitives = new JSPrimitives(genCtx)
6568

@@ -461,14 +464,7 @@ class JSCodeGen()(using genCtx: Context) {
461464
val superClass =
462465
if (sym.is(Trait)) None
463466
else Some(encodeClassNameIdent(sym.superClass))
464-
val jsNativeLoadSpec = {
465-
if (sym.is(Trait)) None
466-
else if (sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) None
467-
else {
468-
val path = fullJSNameOf(sym).split('.').toList
469-
Some(js.JSNativeLoadSpec.Global(path.head, path.tail))
470-
}
471-
}
467+
val jsNativeLoadSpec = computeJSNativeLoadSpecOfClass(sym)
472468

473469
js.ClassDef(
474470
classIdent,
@@ -1008,6 +1004,30 @@ class JSCodeGen()(using genCtx: Context) {
10081004
result
10091005
}
10101006

1007+
private def genExpr(name: JSName)(implicit pos: SourcePosition): js.Tree = name match {
1008+
case JSName.Literal(name) => js.StringLiteral(name)
1009+
case JSName.Computed(sym) => genComputedJSName(sym)
1010+
}
1011+
1012+
private def genComputedJSName(sym: Symbol)(implicit pos: SourcePosition): js.Tree = {
1013+
/* By construction (i.e. restriction in PrepJSInterop), we know that sym
1014+
* must be a static method.
1015+
* Therefore, at this point, we can invoke it by loading its owner and
1016+
* calling it.
1017+
*/
1018+
def moduleOrGlobalScope = genLoadModuleOrGlobalScope(sym.owner)
1019+
def module = genLoadModule(sym.owner)
1020+
1021+
if (sym.owner.isJSType) {
1022+
if (!sym.owner.isNonNativeJSClass || sym.isJSExposed)
1023+
genApplyJSMethodGeneric(sym, moduleOrGlobalScope, args = Nil, isStat = false)
1024+
else
1025+
genApplyJSClassMethod(module, sym, arguments = Nil)
1026+
} else {
1027+
genApplyMethod(module, sym, arguments = Nil)
1028+
}
1029+
}
1030+
10111031
/** Gen JS code for a tree in expression position (in the IR) or the
10121032
* global scope.
10131033
*/
@@ -2096,7 +2116,7 @@ class JSCodeGen()(using genCtx: Context) {
20962116
genApplyStatic(sym, genActualArgs(sym, args))
20972117
} else if (isJSType(sym.owner)) {
20982118
//if (!isScalaJSDefinedJSClass(sym.owner) || isExposed(sym))
2099-
genApplyJSMethodGeneric(tree, sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat)
2119+
genApplyJSMethodGeneric(sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat)(tree.sourcePos)
21002120
/*else
21012121
genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))*/
21022122
} else {
@@ -2115,19 +2135,17 @@ class JSCodeGen()(using genCtx: Context) {
21152135
* - Getters and parameterless methods are translated as `JSBracketSelect`
21162136
* - Setters are translated to `Assign` to `JSBracketSelect`
21172137
*/
2118-
private def genApplyJSMethodGeneric(tree: Tree, sym: Symbol,
2138+
private def genApplyJSMethodGeneric(sym: Symbol,
21192139
receiver: MaybeGlobalScope, args: List[js.TreeOrJSSpread], isStat: Boolean,
21202140
jsSuperClassValue: Option[js.Tree] = None)(
2121-
implicit pos: Position): js.Tree = {
2122-
2123-
implicit val pos: SourcePosition = tree.sourcePos
2141+
implicit pos: SourcePosition): js.Tree = {
21242142

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

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

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

21582176
case _ =>
2159-
def jsFunName = js.StringLiteral(jsNameOf(sym))
2177+
def jsFunName = genExpr(jsNameOf(sym))
21602178

21612179
def genSuperReference(propName: js.Tree): js.Tree = {
21622180
jsSuperClassValue.fold[js.Tree] {
@@ -3479,6 +3497,84 @@ class JSCodeGen()(using genCtx: Context) {
34793497
}
34803498
}
34813499

3500+
private def computeJSNativeLoadSpecOfClass(sym: Symbol): Option[js.JSNativeLoadSpec] = {
3501+
if (sym.is(Trait) || sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) {
3502+
None
3503+
} else {
3504+
atPhase(picklerPhase.next) {
3505+
if (sym.owner.isStaticOwner)
3506+
Some(computeJSNativeLoadSpecOfInPhase(sym))
3507+
else
3508+
None
3509+
}
3510+
}
3511+
}
3512+
3513+
private def computeJSNativeLoadSpecOfInPhase(sym: Symbol)(using Context): js.JSNativeLoadSpec = {
3514+
import js.JSNativeLoadSpec._
3515+
3516+
val symOwner = sym.owner
3517+
3518+
// Marks a code path as unexpected because it should have been reported as an error in `PrepJSInterop`.
3519+
def unexpected(msg: String): Nothing =
3520+
throw new FatalError(i"$msg for ${sym.fullName} at ${sym.srcPos}")
3521+
3522+
if (symOwner.hasAnnotation(jsdefn.JSNativeAnnot)) {
3523+
val jsName = sym.jsName match {
3524+
case JSName.Literal(jsName) => jsName
3525+
case JSName.Computed(_) => unexpected("could not read the simple JS name as a string literal")
3526+
}
3527+
3528+
if (symOwner.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) {
3529+
Global(jsName, Nil)
3530+
} else {
3531+
val ownerLoadSpec = computeJSNativeLoadSpecOfInPhase(symOwner)
3532+
ownerLoadSpec match {
3533+
case Global(globalRef, path) =>
3534+
Global(globalRef, path :+ jsName)
3535+
case Import(module, path) =>
3536+
Import(module, path :+ jsName)
3537+
case ImportWithGlobalFallback(Import(module, modulePath), Global(globalRef, globalPath)) =>
3538+
ImportWithGlobalFallback(
3539+
Import(module, modulePath :+ jsName),
3540+
Global(globalRef, globalPath :+ jsName))
3541+
}
3542+
}
3543+
} else {
3544+
def parsePath(pathName: String): List[String] =
3545+
pathName.split('.').toList
3546+
3547+
def parseGlobalPath(pathName: String): Global = {
3548+
val globalRef :: path = parsePath(pathName)
3549+
Global(globalRef, path)
3550+
}
3551+
3552+
val annot = sym.annotations.find { annot =>
3553+
annot.symbol == jsdefn.JSGlobalAnnot || annot.symbol == jsdefn.JSImportAnnot
3554+
}.getOrElse {
3555+
unexpected("could not find the JS native load spec annotation")
3556+
}
3557+
3558+
if (annot.symbol == jsdefn.JSGlobalAnnot) {
3559+
val pathName = annot.argumentConstantString(0).getOrElse {
3560+
sym.defaultJSName
3561+
}
3562+
parseGlobalPath(pathName)
3563+
} else { // annot.symbol == jsdefn.JSImportAnnot
3564+
val module = annot.argumentConstantString(0).getOrElse {
3565+
unexpected("could not read the module argument as a string literal")
3566+
}
3567+
val path = annot.argumentConstantString(1).fold[List[String]](Nil)(parsePath)
3568+
val importSpec = Import(module, path)
3569+
annot.argumentConstantString(2).fold[js.JSNativeLoadSpec] {
3570+
importSpec
3571+
} { globalPathName =>
3572+
ImportWithGlobalFallback(importSpec, parseGlobalPath(globalPathName))
3573+
}
3574+
}
3575+
}
3576+
}
3577+
34823578
private def isMethodStaticInIR(sym: Symbol): Boolean =
34833579
sym.is(JavaStatic)
34843580

compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ final class JSDefinitions()(using Context) {
3636
def JSPackage_constructorOf(using Context) = JSPackage_constructorOfR.symbol
3737
@threadUnsafe lazy val JSPackage_nativeR = ScalaJSJSPackageClass.requiredMethodRef("native")
3838
def JSPackage_native(using Context) = JSPackage_nativeR.symbol
39+
@threadUnsafe lazy val JSPackage_undefinedR = ScalaJSJSPackageClass.requiredMethodRef("undefined")
40+
def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol
3941

4042
@threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native")
4143
def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass
@@ -50,6 +52,11 @@ final class JSDefinitions()(using Context) {
5052
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
5153
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass
5254

55+
@threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|")
56+
def PseudoUnionModule(using Context) = PseudoUnionModuleRef.symbol
57+
@threadUnsafe lazy val PseudoUnion_fromTypeConstructorR = PseudoUnionModule.requiredMethodRef("fromTypeConstructor")
58+
def PseudoUnion_fromTypeConstructor(using Context) = PseudoUnion_fromTypeConstructorR.symbol
59+
5360
@threadUnsafe lazy val JSArrayType: TypeRef = requiredClassRef("scala.scalajs.js.Array")
5461
def JSArrayClass(using Context) = JSArrayType.symbol.asClass
5562

@@ -63,6 +70,10 @@ final class JSDefinitions()(using Context) {
6370
@threadUnsafe lazy val JavaScriptExceptionType: TypeRef = requiredClassRef("scala.scalajs.js.JavaScriptException")
6471
def JavaScriptExceptionClass(using Context) = JavaScriptExceptionType.symbol.asClass
6572

73+
@threadUnsafe lazy val JSGlobalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSGlobal")
74+
def JSGlobalAnnot(using Context) = JSGlobalAnnotType.symbol.asClass
75+
@threadUnsafe lazy val JSImportAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSImport")
76+
def JSImportAnnot(using Context) = JSImportAnnotType.symbol.asClass
6677
@threadUnsafe lazy val JSGlobalScopeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSGlobalScope")
6778
def JSGlobalScopeAnnot(using Context) = JSGlobalScopeAnnotType.symbol.asClass
6879
@threadUnsafe lazy val JSNameAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSName")
@@ -73,21 +84,24 @@ final class JSDefinitions()(using Context) {
7384
def JSBracketAccessAnnot(using Context) = JSBracketAccessAnnotType.symbol.asClass
7485
@threadUnsafe lazy val JSBracketCallAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSBracketCall")
7586
def JSBracketCallAnnot(using Context) = JSBracketCallAnnotType.symbol.asClass
87+
@threadUnsafe lazy val JSExportTopLevelAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportTopLevel")
88+
def JSExportTopLevelAnnot(using Context) = JSExportTopLevelAnnotType.symbol.asClass
7689
@threadUnsafe lazy val JSExportAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExport")
7790
def JSExportAnnot(using Context) = JSExportAnnotType.symbol.asClass
78-
@threadUnsafe lazy val JSExportDescendentObjectsAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportDescendentObjects")
79-
def JSExportDescendentObjectsAnnot(using Context) = JSExportDescendentObjectsAnnotType.symbol.asClass
80-
@threadUnsafe lazy val JSExportDescendentClassesAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportDescendentClasses")
81-
def JSExportDescendentClassesAnnot(using Context) = JSExportDescendentClassesAnnotType.symbol.asClass
91+
@threadUnsafe lazy val JSExportStaticAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportStatic")
92+
def JSExportStaticAnnot(using Context) = JSExportStaticAnnotType.symbol.asClass
8293
@threadUnsafe lazy val JSExportAllAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportAll")
8394
def JSExportAllAnnot(using Context) = JSExportAllAnnotType.symbol.asClass
84-
@threadUnsafe lazy val JSExportNamedAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportNamed")
85-
def JSExportNamedAnnot(using Context) = JSExportNamedAnnotType.symbol.asClass
86-
@threadUnsafe lazy val RawJSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.RawJSType")
87-
def RawJSTypeAnnot(using Context) = RawJSTypeAnnotType.symbol.asClass
88-
@threadUnsafe lazy val ExposedJSMemberAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.ExposedJSMember")
95+
@threadUnsafe lazy val JSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSType")
96+
def JSTypeAnnot(using Context) = JSTypeAnnotType.symbol.asClass
97+
@threadUnsafe lazy val JSOptionalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSOptional")
98+
def JSOptionalAnnot(using Context) = JSOptionalAnnotType.symbol.asClass
99+
@threadUnsafe lazy val ExposedJSMemberAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.ExposedJSMember")
89100
def ExposedJSMemberAnnot(using Context) = ExposedJSMemberAnnotType.symbol.asClass
90101

102+
@threadUnsafe lazy val JSImportNamespaceModuleRef = requiredModuleRef("scala.scalajs.js.annotation.JSImport.Namespace")
103+
def JSImportNamespaceModule(using Context) = JSImportNamespaceModuleRef.symbol
104+
91105
@threadUnsafe lazy val JSAnyModuleRef = requiredModuleRef("scala.scalajs.js.Any")
92106
def JSAnyModule(using Context) = JSAnyModuleRef.symbol
93107
@threadUnsafe lazy val JSAny_fromFunctionR = (0 to 22).map(n => JSAnyModule.requiredMethodRef("fromFunction" + n)).toArray

compiler/src/dotty/tools/backend/sjs/JSInterop.scala

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,33 @@ import Symbols._
77
import NameOps._
88
import StdNames._
99
import Phases._
10-
import NameKinds.DefaultGetterName
1110

12-
import JSDefinitions._
11+
import dotty.tools.dotc.transform.sjs.JSSymUtils._
1312

14-
/** Management of the interoperability with JavaScript. */
13+
/** Management of the interoperability with JavaScript.
14+
*
15+
* This object only contains forwarders for extension methods in
16+
* `transform.sjs.JSSymUtils`. They are kept to minimize changes in
17+
* `JSCodeGen` in the short term, but it will eventually be removed.
18+
*/
1519
object JSInterop {
1620

1721
/** Is this symbol a JavaScript type? */
18-
def isJSType(sym: Symbol)(using Context): Boolean = {
19-
atPhase(erasurePhase) {
20-
sym.derivesFrom(jsdefn.JSAnyClass) || sym == jsdefn.PseudoUnionClass
21-
}
22-
}
22+
def isJSType(sym: Symbol)(using Context): Boolean =
23+
sym.isJSType
2324

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

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

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

4947
/** Should this symbol be translated into a JS bracket access?
5048
*
5149
* This is true for methods annotated with `@JSBracketAccess`.
5250
*/
5351
def isJSBracketAccess(sym: Symbol)(using Context): Boolean =
54-
sym.hasAnnotation(jsdefn.JSBracketAccessAnnot)
52+
sym.isJSBracketAccess
5553

5654
/** Should this symbol be translated into a JS bracket call?
5755
*
5856
* This is true for methods annotated with `@JSBracketCall`.
5957
*/
6058
def isJSBracketCall(sym: Symbol)(using Context): Boolean =
61-
sym.hasAnnotation(jsdefn.JSBracketCallAnnot)
59+
sym.isJSBracketCall
6260

6361
/** Is this symbol a default param accessor for a JS method?
6462
*
6563
* For default param accessors of *constructors*, we need to test whether
6664
* the companion *class* of the owner is a JS type; not whether the owner
6765
* is a JS type.
6866
*/
69-
def isJSDefaultParam(sym: Symbol)(using Context): Boolean = {
70-
sym.name.is(DefaultGetterName) && {
71-
val owner = sym.owner
72-
if (owner.is(ModuleClass)) {
73-
val isConstructor = sym.name match {
74-
case DefaultGetterName(methName, _) => methName == nme.CONSTRUCTOR
75-
case _ => false
76-
}
77-
if (isConstructor)
78-
isJSType(owner.linkedClass)
79-
else
80-
isJSType(owner)
81-
} else {
82-
isJSType(owner)
83-
}
84-
}
85-
}
67+
def isJSDefaultParam(sym: Symbol)(using Context): Boolean =
68+
sym.isJSDefaultParam
8669

8770
/** Gets the unqualified JS name of a symbol.
8871
*
8972
* If it is not explicitly specified with an `@JSName` annotation, the
9073
* JS name is inferred from the Scala name.
9174
*/
92-
def jsNameOf(sym: Symbol)(using Context): String = {
93-
sym.getAnnotation(jsdefn.JSNameAnnot).flatMap(_.argumentConstant(0)).fold {
94-
val base = sym.name.unexpandedName.decode.toString.stripSuffix("_=")
95-
if (sym.is(ModuleClass)) base.stripSuffix("$")
96-
else if (!sym.is(Method)) base.stripSuffix(" ")
97-
else base
98-
} { constant =>
99-
constant.stringValue
100-
}
101-
}
102-
103-
/** Gets the fully qualified JS name of a static class of module Symbol.
104-
*
105-
* This is the JS name of the symbol qualified by the fully qualified JS
106-
* name of its original owner if the latter is a native JS object.
107-
*/
108-
def fullJSNameOf(sym: Symbol)(using Context): String = {
109-
assert(sym.isClass, s"fullJSNameOf called for non-class symbol $sym")
110-
sym.getAnnotation(jsdefn.JSFullNameAnnot).flatMap(_.argumentConstant(0)).fold {
111-
jsNameOf(sym)
112-
} { constant =>
113-
constant.stringValue
114-
}
115-
}
75+
def jsNameOf(sym: Symbol)(using Context): JSName =
76+
sym.jsName
11677

11778
}

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Compiler {
4141
List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks
4242
List(new semanticdb.ExtractSemanticDB) :: // Extract info into .semanticdb files
4343
List(new PostTyper) :: // Additional checks and cleanups after type checking
44+
List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only)
4445
List(new Staging) :: // Check PCP, heal quoted types and expand macros
4546
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
4647
List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols

0 commit comments

Comments
 (0)