Skip to content

Commit 27be302

Browse files
committed
Fix #1997: @JSSymbol support for native JS classes
We introduce @JSSymbol as a pendant to @JsName to define member access through ES6 symbols. On a high level, the following happens in the compiler: **PrepJSInterop / Definition Site** Take the content of @JSSymbol annotations and put it into a special method, the "symbol forwarder". This is necessary, since otherwise the trees won't get compiled. As a nice side effect, it allows changing the symbol without breaking binary compatibility. Further, we replace the tree inside @JSSymbol with a simple tree calling the symbol forwarder. This tree will not get compiled, but we will only use its symbol. **PrepJSInterop / Call Site** Nothing. **GenJSCode / Definition Site** We emit symbol forwarders as static methods on the native JS class. This does not need an IR change, since this is already supported (but was unused so far). For traits pre 2.12.0, this is a bit more involved, since the symbol forwarder gets moved to the implementation class. We retrieve the implementation class and move the forwarder. **GenJSCode / Call Site** Instead of generating a string literal inside the bracket select, we generate a call to the symbol forwarder. The symbol forwarder's symbol is conveniently available inside the tree of the @JSSymbol annotation. **Caveat** Since we generate number suffixes to disambiguate symbol forwarder overloads, adding an overload to a method with @JSSymbol is a potentially binary breaking change.
1 parent ebd3600 commit 27be302

File tree

7 files changed

+429
-40
lines changed

7 files changed

+429
-40
lines changed

compiler/src/main/scala/org/scalajs/core/compiler/GenJSCode.scala

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,11 @@ abstract class GenJSCode extends plugins.PluginComponent
183183

184184
// Global class generation state -------------------------------------------
185185

186-
private val lazilyGeneratedAnonClasses = mutable.Map.empty[Symbol, ClassDef]
186+
private val lazilyGeneratedClasses = mutable.Map.empty[Symbol, ClassDef]
187187
private val generatedClasses =
188188
ListBuffer.empty[(Symbol, Option[String], js.ClassDef)]
189189

190-
private def consumeLazilyGeneratedAnonClass(sym: Symbol): ClassDef = {
190+
private def consumeLazilyGeneratedClass(sym: Symbol): ClassDef = {
191191
/* If we are trying to generate an method as JSFunction, we cannot
192192
* actually consume the symbol, since we might fail trying and retry.
193193
* We will then see the same tree again and not find the symbol anymore.
@@ -197,9 +197,9 @@ abstract class GenJSCode extends plugins.PluginComponent
197197
*/
198198
val optDef = {
199199
if (tryingToGenMethodAsJSFunction)
200-
lazilyGeneratedAnonClasses.get(sym)
200+
lazilyGeneratedClasses.get(sym)
201201
else
202-
lazilyGeneratedAnonClasses.remove(sym)
202+
lazilyGeneratedClasses.remove(sym)
203203
}
204204

205205
optDef.getOrElse {
@@ -233,7 +233,7 @@ abstract class GenJSCode extends plugins.PluginComponent
233233
*
234234
* Other ClassDefs are emitted according to their nature:
235235
* * Scala.js-defined JS class -> `genScalaJSDefinedJSClass()`
236-
* * Other raw JS type (<: js.Any) -> `genRawJSClassData()`
236+
* * Other raw JS type (<: js.Any) -> `genRawJSClass()`
237237
* * Interface -> `genInterface()`
238238
* * Implementation class -> `genImplClass()`
239239
* * Normal class -> `genClass()`
@@ -249,8 +249,8 @@ abstract class GenJSCode extends plugins.PluginComponent
249249
}
250250
val allClassDefs = collectClassDefs(cunit.body)
251251

252-
/* There are three types of anonymous classes we want to generate
253-
* only once we need them so we can inline them at construction site:
252+
/* There are four types of classes we want to generate only once we need
253+
* them so we can inline them at usage site:
254254
*
255255
* - lambdas for js.FunctionN and js.ThisFunctionN (SAMs). (We may not
256256
* generate actual Scala classes for these).
@@ -260,33 +260,36 @@ abstract class GenJSCode extends plugins.PluginComponent
260260
* - lambdas for scala.FunctionN. This is only an optimization and may
261261
* fail. In the case of failure, we fall back to generating a
262262
* fully-fledged Scala class.
263+
* - Implementation classes for native JS classes. The only useful
264+
* thing they contain are symbol forwarders for @JSSymbol access.
265+
* We want to emit the symbol forwarders as static methods on the
266+
* native JS class so we do not need to generate another class file.
263267
*
264268
* Since for all these, we don't know how they inter-depend, we just
265269
* store them in a map at this point.
266270
*/
267-
val (lazyAnons, fullClassDefs) = allClassDefs.partition { cd =>
271+
def isRawJSImplClass(sym: Symbol) = {
272+
sym.isImplClass && isRawJSType(sym.owner.info.decl(
273+
sym.name.dropRight(nme.IMPL_CLASS_SUFFIX.length)).tpe)
274+
}
275+
276+
val (lazyClasses, fullClassDefs) = allClassDefs.partition { cd =>
268277
val sym = cd.symbol
269278
isRawJSFunctionDef(sym) || sym.isAnonymousFunction ||
270-
isScalaJSDefinedAnonJSClass(sym)
279+
isScalaJSDefinedAnonJSClass(sym) || isRawJSImplClass(sym)
271280
}
272281

273-
lazilyGeneratedAnonClasses ++= lazyAnons.map(cd => cd.symbol -> cd)
282+
lazilyGeneratedClasses ++= lazyClasses.map(cd => cd.symbol -> cd)
274283

275284
/* Finally, we emit true code for the remaining class defs. */
276285
for (cd <- fullClassDefs) {
277286
val sym = cd.symbol
278287
implicit val pos = sym.pos
279288

280289
/* Do not actually emit code for primitive types nor scala.Array. */
281-
val isPrimitive =
282-
isPrimitiveValueClass(sym) || (sym == ArrayClass)
290+
val isPrimitive = isPrimitiveValueClass(sym) || (sym == ArrayClass)
283291

284-
/* Similarly, do not emit code for impl classes of raw JS traits. */
285-
val isRawJSImplClass =
286-
sym.isImplClass && isRawJSType(
287-
sym.owner.info.decl(sym.name.dropRight(nme.IMPL_CLASS_SUFFIX.length)).tpe)
288-
289-
if (!isPrimitive && !isRawJSImplClass) {
292+
if (!isPrimitive) {
290293
withScopedVars(
291294
currentClassSym := sym,
292295
unexpectedMutatedFields := mutable.Set.empty,
@@ -298,7 +301,7 @@ abstract class GenJSCode extends plugins.PluginComponent
298301
if (!sym.isTraitOrInterface && isScalaJSDefinedJSClass(sym))
299302
genScalaJSDefinedJSClass(cd)
300303
else
301-
genRawJSClassData(cd)
304+
genRawJSClass(cd)
302305
} else if (sym.isTraitOrInterface) {
303306
genInterface(cd)
304307
} else if (sym.isImplClass) {
@@ -319,7 +322,7 @@ abstract class GenJSCode extends plugins.PluginComponent
319322
genIRFile(cunit, sym, suffix, tree)
320323
}
321324
} finally {
322-
lazilyGeneratedAnonClasses.clear()
325+
lazilyGeneratedClasses.clear()
323326
generatedClasses.clear()
324327
pos2irPosCache.clear()
325328
}
@@ -531,7 +534,7 @@ abstract class GenJSCode extends plugins.PluginComponent
531534
"Generating AnonSJSDefinedNew of non anonymous SJSDefined JS class")
532535

533536
// Find the ClassDef for this anonymous class
534-
val classDef = consumeLazilyGeneratedAnonClass(sym)
537+
val classDef = consumeLazilyGeneratedClass(sym)
535538

536539
// Generate a normal SJSDefinedJSClass
537540
val origJsClass =
@@ -685,9 +688,8 @@ abstract class GenJSCode extends plugins.PluginComponent
685688

686689
// Generate the class data of a raw JS class -------------------------------
687690

688-
/** Gen the IR ClassDef for a raw JS class or trait.
689-
*/
690-
def genRawJSClassData(cd: ClassDef): js.ClassDef = {
691+
/** Gen the IR ClassDef for a raw JS class or trait. */
692+
def genRawJSClass(cd: ClassDef): js.ClassDef = {
691693
val sym = cd.symbol
692694
implicit val pos = sym.pos
693695

@@ -704,8 +706,56 @@ abstract class GenJSCode extends plugins.PluginComponent
704706
if (sym.isTraitOrInterface) None
705707
else Some(jsNativeLoadSpecOf(sym))
706708

709+
lazy val implMethodsByName: Map[String, DefDef] = {
710+
val implClassDef = consumeLazilyGeneratedClass(sym.implClass)
711+
712+
def gen(tree: Tree): List[(String, DefDef)] = tree match {
713+
case Template(_, _, body) => body.flatMap(gen)
714+
715+
case dd: DefDef =>
716+
List(dd.symbol.unexpandedName.encoded -> dd)
717+
718+
case _ => Nil
719+
}
720+
721+
gen(implClassDef.impl).toMap
722+
}
723+
724+
// Generates symbol forwarders
725+
def gen(tree: Tree): List[js.MethodDef] = tree match {
726+
case Template(_, _, body) => body.flatMap(gen)
727+
728+
case dd: DefDef if jsInterop.isSymbolForwarder(dd.symbol) =>
729+
val sym = dd.symbol
730+
val patchedDef = {
731+
if (scalaUsesImplClasses && sym.owner.isTraitOrInterface) {
732+
assert(sym.isDeferred, "Found non-abstract method in trait at " +
733+
s"${dd.pos}: ${sym.fullName}")
734+
735+
/* We grab the body from the implementation class. This does not
736+
* work so directly in general, since the parameter symbols and
737+
* the `this` reference would be wrong in the body. Here it works,
738+
* because we have a static, parameterless method.
739+
*/
740+
val nrhs = implMethodsByName(sym.unexpandedName.encoded).rhs
741+
treeCopy.DefDef(dd, dd.mods, dd.name, dd.tparams,
742+
dd.vparamss, dd.tpt, nrhs)
743+
} else {
744+
assert(!sym.isDeferred, "Found an abstract symbol forwarder at " +
745+
s"${dd.pos}: ${sym.fullName}")
746+
dd // No patching necessary.
747+
}
748+
}
749+
750+
genMethod(patchedDef).toList
751+
752+
case _ => Nil
753+
}
754+
755+
val generatedMethods = Hashers.hashDefs(gen(cd.impl))
756+
707757
js.ClassDef(classIdent, kind, superClass, genClassInterfaces(sym),
708-
jsNativeLoadSpec, Nil)(
758+
jsNativeLoadSpec, generatedMethods)(
709759
OptimizerHints.empty)
710760
}
711761

@@ -1255,7 +1305,8 @@ abstract class GenJSCode extends plugins.PluginComponent
12551305
if (scalaPrimitives.isPrimitive(sym) &&
12561306
!jsPrimitives.shouldEmitPrimitiveBody(sym)) {
12571307
None
1258-
} else if (isAbstractMethod(dd)) {
1308+
} else if (isAbstractMethod(dd) && !(scalaUsesImplClasses &&
1309+
jsInterop.isSymbolForwarder(sym))) {
12591310
val body = if (scalaUsesImplClasses &&
12601311
sym.hasAnnotation(JavaDefaultMethodAnnotation)) {
12611312
/* For an interface method with @JavaDefaultMethod, make it a
@@ -1297,10 +1348,13 @@ abstract class GenJSCode extends plugins.PluginComponent
12971348
case _ => false
12981349
}
12991350

1351+
val isSymbolForwarder = jsInterop.isSymbolForwarder(sym)
1352+
13001353
val shouldMarkInline = {
13011354
sym.hasAnnotation(InlineAnnotationClass) ||
13021355
sym.name.startsWith(nme.ANON_FUN_NAME) ||
1303-
adHocInlineMethods.contains(sym.fullName)
1356+
adHocInlineMethods.contains(sym.fullName) ||
1357+
isSymbolForwarder
13041358
}
13051359

13061360
val shouldMarkNoinline = {
@@ -1328,8 +1382,9 @@ abstract class GenJSCode extends plugins.PluginComponent
13281382
Some(genStat(rhs)))(optimizerHints, None)
13291383
} else {
13301384
val resultIRType = toIRType(sym.tpe.resultType)
1331-
genMethodDef(static = sym.owner.isImplClass, methodName,
1332-
params, resultIRType, rhs, optimizerHints)
1385+
val static = sym.owner.isImplClass || isSymbolForwarder
1386+
genMethodDef(static, methodName, params, resultIRType, rhs,
1387+
optimizerHints)
13331388
}
13341389
}
13351390

@@ -2173,10 +2228,10 @@ abstract class GenJSCode extends plugins.PluginComponent
21732228
} else if (isHijackedBoxedClass(clsSym)) {
21742229
genNewHijackedBoxedClass(clsSym, ctor, args map genExpr)
21752230
} else if (isRawJSFunctionDef(clsSym)) {
2176-
val classDef = consumeLazilyGeneratedAnonClass(clsSym)
2231+
val classDef = consumeLazilyGeneratedClass(clsSym)
21772232
genRawJSFunction(classDef, args.map(genExpr))
21782233
} else if (clsSym.isAnonymousFunction) {
2179-
val classDef = consumeLazilyGeneratedAnonClass(clsSym)
2234+
val classDef = consumeLazilyGeneratedClass(clsSym)
21802235
tryGenAnonFunctionClass(classDef, args.map(genExpr)).getOrElse {
21812236
// Cannot optimize anonymous function class. Generate full class.
21822237
generatedClasses +=
@@ -3949,7 +4004,8 @@ abstract class GenJSCode extends plugins.PluginComponent
39494004
def hasExplicitJSEncoding =
39504005
sym.hasAnnotation(JSNameAnnotation) ||
39514006
sym.hasAnnotation(JSBracketAccessAnnotation) ||
3952-
sym.hasAnnotation(JSBracketCallAnnotation)
4007+
sym.hasAnnotation(JSBracketCallAnnotation) ||
4008+
sym.hasAnnotation(JSSymbolAnnotation)
39534009

39544010
val boxedResult = sym.name match {
39554011
case JSUnaryOpMethodName(code) if argc == 0 =>
@@ -3969,7 +4025,13 @@ abstract class GenJSCode extends plugins.PluginComponent
39694025
js.JSFunctionApply(receiver, args)
39704026

39714027
case _ =>
3972-
def jsFunName = js.StringLiteral(jsNameOf(sym))
4028+
def jsFunName: js.Tree = {
4029+
sym.getAnnotation(JSSymbolAnnotation).fold[js.Tree] {
4030+
js.StringLiteral(jsNameOf(sym))
4031+
} { annot =>
4032+
genApplyStatic(annot.args(0).symbol, Nil)
4033+
}
4034+
}
39734035

39744036
def genSuperReference(propName: js.Tree): js.Tree = {
39754037
superIn.fold[js.Tree] {

compiler/src/main/scala/org/scalajs/core/compiler/JSDefinitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ trait JSDefinitions { self: JSGlobalAddons =>
5555
lazy val JavaScriptExceptionClass = getClassIfDefined("scala.scalajs.js.JavaScriptException")
5656

5757
lazy val JSNameAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSName")
58+
lazy val JSSymbolAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSSymbol")
5859
lazy val JSFullNameAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSFullName")
5960
lazy val JSBracketAccessAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSBracketAccess")
6061
lazy val JSBracketCallAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSBracketCall")

compiler/src/main/scala/org/scalajs/core/compiler/JSGlobalAddons.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ trait JSGlobalAddons extends JSDefinitions
4343
private val jsNativeLoadSpecs =
4444
mutable.Map.empty[Symbol, JSNativeLoadSpec]
4545

46-
private val exportPrefix = "$js$exported$"
46+
private val jsPrefix = "$js$"
47+
private val exportPrefix = jsPrefix + "exported$"
4748
private val methodExportPrefix = exportPrefix + "meth$"
4849
private val propExportPrefix = exportPrefix + "prop$"
50+
private val symbolForwarderPrefix = jsPrefix + "jssym$"
4951

5052
trait ExportInfo {
5153
val jsName: String
@@ -86,6 +88,14 @@ trait JSGlobalAddons extends JSDefinitions
8688
sym.unexpandedName.startsWith(exportPrefix) &&
8789
!sym.hasFlag(Flags.DEFAULTPARAM)
8890

91+
def symbolForwarderName(target: Symbol, num: Int): TermName = {
92+
val suffix = target.unexpandedName.encodedName + "$" + num
93+
newTermName(symbolForwarderPrefix + suffix)
94+
}
95+
96+
def isSymbolForwarder(sym: Symbol): Boolean =
97+
sym.unexpandedName.startsWith(symbolForwarderPrefix)
98+
8999
/** retrieves the originally assigned jsName of this export and whether it
90100
* is a property
91101
*/

0 commit comments

Comments
 (0)