-
Notifications
You must be signed in to change notification settings - Fork 396
Fix #1997: Add support for js.Symbols in @JSName annotations. #2659
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
Conversation
Review by @sjrd |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3496/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3495/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed until JSDefinitions.scala
so far. Will continue later.
} | ||
|
||
@inline def apply(): Symbol = NativeSymbol() | ||
@inline def apply(description: String): Symbol = NativeSymbol() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NativeSymbol(description)
?
* - Implementation classes for native JS classes. The only useful | ||
* thing they contain are symbol forwarders for @JSSymbol access. | ||
* We want to emit the symbol forwarders as static methods on the | ||
* native JS class so we do not need to generate another class file. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another sjsir file?
val patchedDef = { | ||
if (scalaUsesImplClasses && sym.owner.isTraitOrInterface) { | ||
assert(sym.isDeferred, "Found non-abstract method in trait at " + | ||
s"${dd.pos}: ${sym.fullName}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4 spaces
s"${dd.pos}: ${sym.fullName}") | ||
|
||
/* We grab the body from the implementation class. This does not | ||
* work so directly in general, since the parameter symbols and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"so directly" does not sound right.
} else { | ||
assert(!sym.isDeferred, "Found an abstract symbol forwarder at " + | ||
s"${dd.pos}: ${sym.fullName}") | ||
dd // No patching necessary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are 2 spaces between dd
and //
@@ -1255,7 +1305,8 @@ abstract class GenJSCode extends plugins.PluginComponent | |||
if (scalaPrimitives.isPrimitive(sym) && | |||
!jsPrimitives.shouldEmitPrimitiveBody(sym)) { | |||
None | |||
} else if (isAbstractMethod(dd)) { | |||
} else if (isAbstractMethod(dd) && !(scalaUsesImplClasses && | |||
jsInterop.isSymbolForwarder(sym))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is scalaUsesImplClasses &&
necessary here?
If yes, can you put it on the next line, so that the line break goes with the outer-most &&
rather than the inner-most one?
In general, I'm concerned that we add this for native JS classes without knowing what to do about Scala.js-defined JS classes. I would like both things to be addressed at the same time. For Scala.js-defined JS classes, it means we need a definition-time access to calling the mentioned symbol. This in turn means that we probably need to create Scala.js-defined JS classes lazily, when they are first required. (this might mean immediately if they are exported). |
That is a very good point :-/ I already had the feeling that we should impose some restrictions on the contents of |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3601/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3646/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3670/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3675/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3676/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3677/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3678/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3679/ |
Yeah, managed to make the CI pass again. Now on to write more tests! |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3680/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3681/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3682/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3683/ |
Since we decided to restrict |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3684/ |
Euh, how do you handle nested classes / objects that have a |
Aaannd ... rebased and cleaned up the history! Sending back the review by @gzm0 now ;) Edit: GitHub won't let me assign you as a reviewer, probably because you created the PR. |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3696/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also seems I won't be able to approve my own PR :P
@@ -12,7 +12,7 @@ package org.scalajs.core.tools.linker.backend.emitter | |||
import scala.collection.mutable | |||
|
|||
import org.scalajs.core.ir.ClassKind | |||
import org.scalajs.core.ir.Trees.{FieldDef, JSNativeLoadSpec} | |||
import org.scalajs.core.ir.Trees.{FieldDef, JSNativeLoadSpec, Ident} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still useless import.
def keyFor(sym: Symbol): js.UndefOr[String] = js.native | ||
|
||
val iterator: Symbol = js.native | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took this from your PR, but actually: Why do we have the NativeSymbol
inside? Can't we just have @JSName('for')
for forKey
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because at some point we realized we would have want js.Array
to be non-native, to include custom things in there. I'm mostly anticipating the need to evolve Symbol
into more than just its facade.
Also, for 1.0 I think we should turn the type js.Symbol
into a hijacked class. We have hijacked classes to represent all other JS primitive types, but not symbol
. I think it would make sense. In that case, it would also be weird, possibly problematic, for the companion object to be a native.
But I don't have very strong feelings about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't you mean turning scala.Symbol
into a hijacked class? I do get the story for js.Array
, but js.Array
is also much closer to the runtime. And I think between 0.6 and 1.0 its ok to have the theoretically source incompatible change of making js.Symbol
not extend js.Any
anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I really mean js.Symbol
. scala.Symbol
we already ruled out, because their semantics are too different from js.Symbol
(#1541). However we should be able to do things like
x match {
case x: js.Symbol =>
}
and we can only do that if x.isInstanceOf[js.Symbol]
has a meaningful spec, i.e., type of x === 'symbol'
. And that, we can only do if js.Symbol
is a hijacked class.
I do get the story for js.Array, but js.Array is also much closer to the runtime.
IMO js.Symbol
is much closer to the run-time than js.Array
. For js.Array
we're only optimizing its apply
method a bit. js.Symbol
s creep into the very definition of the language, now that they're supported in @JSName
.
And I think between 0.6 and 1.0 its ok to have the theoretically source incompatible change of making js.Symbol not extend js.Any anymore.
OK. Let's have an @js.native object Symbol
for now, then.
@inline def forKey(key: String): Symbol = NativeSymbol.`for`(key) | ||
@inline def keyFor(sym: Symbol): js.UndefOr[String] = NativeSymbol.keyFor(sym) | ||
|
||
@inline def iterator: Symbol = NativeSymbol.iterator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This must be a stable identifier. Otherwise defining an iterable will be problematic :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ouch ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And now that I think of it, we should probably add all well known symbols here.
genApplyJSClassMethod(module, sym, arguments = Nil) | ||
} else { | ||
genApplyMethod(module, sym, arguments = Nil) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found this part particularly ugly when I wrote it. I guess you also tried and failed to consolidate with genApply
:-/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't really try that, because things are decomposed early into JS/Scala calls in the genApply
giant dispatch. So seeing them all together like this never came up before. I think it's OK like that, if a bit ugly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. But what we should probably do, is add tests for all code paths here. Currently we only read symbols for definitions from Scala modules.
We probably should also have this nasty test case here:
@ScalaJSDefined
object Foo extends js.Any {
val sym = js.Symbol()
@JSName(sym)
def yolo(): Int = 1
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should also have this nasty test case here:
Well, that one doesn't work. It stack overflows. It makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we warn? Just like bad initialization order? (Not in this PR for sure).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -239,6 +239,11 @@ trait JSEncoding extends SubComponent { self: GenJSCode => | |||
def needsModuleClassSuffix(sym: Symbol): Boolean = | |||
sym.isModuleClass && !foreignIsImplClass(sym) | |||
|
|||
def encodeComputedNameIdentity(sym: Symbol): String = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should specify that this is compiler-private: Nothing may make any assumptions about the computed names of anything (except for their identity and starting with __computed
). I realized that I have not documented this anywhere (was just documented in my head :P).
An alternative location might be the doc of PropertyName#encodedName
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PropertyName#encodedName
seems a reasonable place for that.
|newSource1.scala:11: error: Implementation restriction: @JSName with a js.Symbol is not supported on nested native classes and objects | ||
| @JSName(Sym.sym) | ||
| ^ | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I would make the distinction between this error message and the one above (at this point).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message above does not work here, because A
really is a member of a JS type (Enclosing
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Also since this works for objects, I'm fine with this.
| | ||
| abstract class C extends A with B | ||
| ^ | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never realized that this is only a warning. Will this become an error? If no, we should:
- Have a valid use case for it.
- A way to silence the warning.
Both of these are of course completely orthogonal to this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically all warnings of PrepJSInterop will become errors in 1.0. #1111
val asTrait: OverloadedRuntimeDispatchMethodTrait = obj | ||
assertEquals(6, asTrait.foo(3)) | ||
assertEquals("Hello Moon", asTrait.foo("Moon")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all of the sjs defined classes, I think we should also check that accessing them via the dynamic interface works. (meaning for now, a handwritten dynamic interface).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, yes. It's kind of a consequence of the fact that reading via the trait works + the previous test passes. But it's not very self-contained. I'll add a "dynamic"-based test as well.
val value = genApplyMethod(tupRef, js.Ident("$$und2__O"), Nil, | ||
jstpe.AnyType) | ||
keyToPropName(key, index) -> value | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't you want to retain the literals where you can here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optimizer will take care of that, if necessary.
if (tuples.size == exprs.size) | ||
Some(tuples) | ||
else | ||
None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OMG, I'm turning to the dark side... I just thought that this would be a prime use-case for
exps.sequence(Tuple2.unapply _)
except that Scala's List doesn't have a sequencing operator...
The encodedName is an identity we can use internally to track methods. However, it does not coincide with any real name, as we will see with the introduction of ComputedNames.
Updated. |
I locally tested that we can depend on uTest and ScalaTest, and use them to test things with this branch. I believe this should give enough confidence regarding the ability to read old IR. |
The `@JSName` annotation could previously be used to specify the JavaScript *string* name of a property/method, independently of its Scala name. This can be used both in native JS classes as well as Scala.js-defined JS classes. This commit extends the capability of `@JSName` to admit a (static, stable) reference to a `js.Symbol`. This applies to both native and Scala.js-defined JS types. At call site, the semantics and compilation scheme are straightforward: it is a `JSBracketSelect` whose `item` part is a call to the symbol accessor. Since it is static by construction, we can call it from anywhere. At definition site for native JS types, there is nothing to do (besides validity checks). At definition site for Scala.js-defined JS classes, things get tricky. We can now have "exported" class members whose name is not a constant string anymore. This requires to extend the IR with `ComputedName`s (a concept that exists in ECMAScript 2015). A computed name is an arbitrary expression `Tree` that is evaluated to obtain the dynamic name of a property. This complicates the Emitter somewhat, but not that much because Scala.js-defined JS classes are created lazily, so we can execute arbitrary user-defined code at class-definition time. For overloading resolution, `ComputedName`s have a `logicalName`, derived from the fully qualified name of the symbol accessors. All `ComputedName`'d properties/methods with the same logical name (i.e., referencing the same static stable `js.Symbol`) are considered to be overloaded. This has an unavoidable caveat: if two `ComputedName`s in the same class have a different logical name, but actually evaluate to the same symbol, things will go wrong at run-time. `JSObjectConstr` is enhanced to support `ComputedName`s as well, for consistency. It is also consistent with Object Initializers in ECMAScript 2015. This actually complicates their treatment, mostly because of evaluation order concerns. Exports (in Scala classes and at the top-level) cannot use symbolic names. This is not expected to change in the future. There is one implementation restriction: `@JSName` with a symbol is not supported on native JS classes and objects that are nested within a native JS object. There is no language reason to disallow this, but the implementation is surprisingly difficult, due to the way scalac flattens such entities.
Now that `JSObjectConstr` admits `ComputedName`s for keys, there are more patterns of calls of `js.Dynamic.literal` that can be emitted as `JSObjectConstr`. This commit leverages that.
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3706/ |
Refer to this link for build results (access rights to CI server needed): https://scala-webapps.epfl.ch/jenkins/job/scalajs-pr/3707/ |
LGTM (I can't approve the PR since it is mine). |
I'll let you approve and merge (unless you have concerns :)). |
Wooot 🎉 |
This commit implements the entire specification of non-native JS classes, including nested ones, except the behavior of *anonymous* JS classes. Anonymous JS classes are not supposed to have their own class/prototype, but currently they still do. The most important PRs to scala-js/scala-js that define what is implemented here were: 1. scala-js/scala-js#1809 Essentials of Scala.js-defined classes (former name of non-native JS classes) 2. scala-js/scala-js#1982 Support for secondary constructors 3. scala-js/scala-js#2186 Support for default parameters in constructors 4. scala-js/scala-js#2659 Support for `js.Symbol`s in `@JSName` annotations 5. scala-js/scala-js#3161 Nested JS classes However, this commit was written more based on the state of things at Scala.js v1.2.0 rather than as a port of the abovementioned PRs. The support is spread over 4 components: * The `ExplicitJSClasses` phase, which reifies all nested JS classes, and has extensive documentation in the code. * The `JSExportsGen` component of the back-end, which creates dispatchers for run-time overloading, default parameters and variadic arguments (equivalent to `GenJSExports` in Scala 2). * The `JSConstructorGen` component, which massages the constructors of JS classes and their dispatcher into a unique JS constructor. * Bits and pieces in `JSCodeGen`, notably to generate the `js.ClassDef`s for non-native JS classes, orchestrate the two other back-end components, and to adapt calls to the members of non-native JS classes. `JSConstructorGen` in particular is copied quasi-verbatim from pieces of `GenJSCode` in Scala 2, since it works on `js.IRNode`s, without knowledge of scalac/dotc data structures.
This commit implements the entire specification of non-native JS classes, including nested ones, except the behavior of *anonymous* JS classes. Anonymous JS classes are not supposed to have their own class/prototype, but currently they still do. The most important PRs to scala-js/scala-js that define what is implemented here were: 1. scala-js/scala-js#1809 Essentials of Scala.js-defined classes (former name of non-native JS classes) 2. scala-js/scala-js#1982 Support for secondary constructors 3. scala-js/scala-js#2186 Support for default parameters in constructors 4. scala-js/scala-js#2659 Support for `js.Symbol`s in `@JSName` annotations 5. scala-js/scala-js#3161 Nested JS classes However, this commit was written more based on the state of things at Scala.js v1.2.0 rather than as a port of the abovementioned PRs. The support is spread over 4 components: * The `ExplicitJSClasses` phase, which reifies all nested JS classes, and has extensive documentation in the code. * The `JSExportsGen` component of the back-end, which creates dispatchers for run-time overloading, default parameters and variadic arguments (equivalent to `GenJSExports` in Scala 2). * The `JSConstructorGen` component, which massages the constructors of JS classes and their dispatcher into a unique JS constructor. * Bits and pieces in `JSCodeGen`, notably to generate the `js.ClassDef`s for non-native JS classes, orchestrate the two other back-end components, and to adapt calls to the members of non-native JS classes. `JSConstructorGen` in particular is copied quasi-verbatim from pieces of `GenJSCode` in Scala 2, since it works on `js.IRNode`s, without knowledge of scalac/dotc data structures.
This commit implements the entire specification of non-native JS classes, including nested ones, except the behavior of *anonymous* JS classes. Anonymous JS classes are not supposed to have their own class/prototype, but currently they still do. The most important PRs to scala-js/scala-js that define what is implemented here were: 1. scala-js/scala-js#1809 Essentials of Scala.js-defined classes (former name of non-native JS classes) 2. scala-js/scala-js#1982 Support for secondary constructors 3. scala-js/scala-js#2186 Support for default parameters in constructors 4. scala-js/scala-js#2659 Support for `js.Symbol`s in `@JSName` annotations 5. scala-js/scala-js#3161 Nested JS classes However, this commit was written more based on the state of things at Scala.js v1.2.0 rather than as a port of the abovementioned PRs. The support is spread over 4 components: * The `ExplicitJSClasses` phase, which reifies all nested JS classes, and has extensive documentation in the code. * The `JSExportsGen` component of the back-end, which creates dispatchers for run-time overloading, default parameters and variadic arguments (equivalent to `GenJSExports` in Scala 2). * The `JSConstructorGen` component, which massages the constructors of JS classes and their dispatcher into a unique JS constructor. * Bits and pieces in `JSCodeGen`, notably to generate the `js.ClassDef`s for non-native JS classes, orchestrate the two other back-end components, and to adapt calls to the members of non-native JS classes. `JSConstructorGen` in particular is copied quasi-verbatim from pieces of `GenJSCode` in Scala 2, since it works on `js.IRNode`s, without knowledge of scalac/dotc data structures.
This commit implements the entire specification of non-native JS classes, including nested ones, except the behavior of *anonymous* JS classes. Anonymous JS classes are not supposed to have their own class/prototype, but currently they still do. The most important PRs to scala-js/scala-js that define what is implemented here were: 1. scala-js/scala-js#1809 Essentials of Scala.js-defined classes (former name of non-native JS classes) 2. scala-js/scala-js#1982 Support for secondary constructors 3. scala-js/scala-js#2186 Support for default parameters in constructors 4. scala-js/scala-js#2659 Support for `js.Symbol`s in `@JSName` annotations 5. scala-js/scala-js#3161 Nested JS classes However, this commit was written more based on the state of things at Scala.js v1.2.0 rather than as a port of the abovementioned PRs. The support is spread over 4 components: * The `ExplicitJSClasses` phase, which reifies all nested JS classes, and has extensive documentation in the code. * The `JSExportsGen` component of the back-end, which creates dispatchers for run-time overloading, default parameters and variadic arguments (equivalent to `GenJSExports` in Scala 2). * The `JSConstructorGen` component, which massages the constructors of JS classes and their dispatcher into a unique JS constructor. * Bits and pieces in `JSCodeGen`, notably to generate the `js.ClassDef`s for non-native JS classes, orchestrate the two other back-end components, and to adapt calls to the members of non-native JS classes. `JSConstructorGen` in particular is copied quasi-verbatim from pieces of `GenJSCode` in Scala 2, since it works on `js.IRNode`s, without knowledge of scalac/dotc data structures.
(Old PR description below)
The
@JSName
annotation could previously be used to specify the JavaScript string name of a property/method, independently of its Scala name. This can be used both in native JS classes as well as Scala.js-defined JS classes.This commit extends the capability of
@JSName
to admit a (static, stable) reference to ajs.Symbol
. This applies to both native and Scala.js-defined JS types.At call site, the semantics and compilation scheme are straightforward: it is a
JSBracketSelect
whoseitem
part is a call to the symbol accessor. Since it is static by construction, we can call it from anywhere.At definition site for native JS types, there is nothing to do (besides validity checks).
At definition site for Scala.js-defined JS classes, things get tricky. We can now have "exported" class members whose name is not a constant string anymore. This requires to extend the IR with
ComputedName
s (a concept that exists in ECMAScript 2015). A computed name is an arbitrary expressionTree
that is evaluated to obtain the dynamic name of a property. This complicates the Emitter somewhat, but not that much because Scala.js-defined JS classes are created lazily, so we can execute arbitrary user-defined code at class-definition time.For overloading resolution,
ComputedName
s have alogicalName
, derived from the fully qualified name of the symbol accessors. AllComputedName
'd properties/methods with the same logical name (i.e., referencing the same static stablejs.Symbol
) are considered to be overloaded. This has an unavoidable caveat: if twoComputedName
s in the same class have a different logical name, but actually evaluate to the same symbol, things will go wrong at run-time.JSObjectConstr
is enhanced to supportComputedName
s as well, for consistency. It is also consistent with Object Initializers in ECMAScript 2015. This actually complicates their treatment, mostly because of evaluation order concerns.Exports (in Scala classes and at the top-level) cannot use symbolic names. This is not expected to change in the future.
There is one implementation restriction:
@JSName
with a symbol is not supported on native JS classes and objects that are nested within a native JS object. There is no language reason to disallow this, but the implementation is surprisingly difficult, due to the way scalac flattens such entities.Old PR description
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.