Skip to content

Commit 0011538

Browse files
authored
Merge pull request #9955 from dotty-staging/fix-9809
Fix #9809: enable static fields in Scala.js and change forwarders
2 parents 84e28e8 + dc31c9d commit 0011538

File tree

5 files changed

+262
-12
lines changed

5 files changed

+262
-12
lines changed

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

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ class JSCodeGen()(using genCtx: Context) {
773773
// Generate the fields of a class ------------------------------------------
774774

775775
/** Gen definitions for the fields of a class. */
776-
private def genClassFields(td: TypeDef): List[js.AnyFieldDef] = {
776+
private def genClassFields(td: TypeDef): List[js.MemberDef] = {
777777
val classSym = td.symbol.asClass
778778
assert(currentClassSym.get == classSym,
779779
"genClassFields called with a ClassDef other than the current one")
@@ -785,19 +785,42 @@ class JSCodeGen()(using genCtx: Context) {
785785
!f.isOneOf(Method | Module) && f.isTerm
786786
&& !f.hasAnnotation(jsdefn.JSNativeAnnot)
787787
&& !f.hasAnnotation(jsdefn.JSOptionalAnnot)
788-
}.map({ f =>
788+
}.flatMap({ f =>
789789
implicit val pos = f.span
790790

791-
val flags = js.MemberFlags.empty.withMutable(f.is(Mutable))
791+
val isStaticField = f.is(JavaStatic).ensuring(isStatic => !(isStatic && isJSClass))
792+
793+
val namespace = if isStaticField then js.MemberNamespace.PublicStatic else js.MemberNamespace.Public
794+
val mutable = isStaticField || f.is(Mutable)
795+
796+
val flags = js.MemberFlags.empty.withMutable(mutable).withNamespace(namespace)
792797

793798
val irTpe =
794799
if (isJSClass) genExposedFieldIRType(f)
795800
else toIRType(f.info)
796801

797802
if (isJSClass && f.isJSExposed)
798-
js.JSFieldDef(flags, genExpr(f.jsName)(f.sourcePos), irTpe)
803+
js.JSFieldDef(flags, genExpr(f.jsName)(f.sourcePos), irTpe) :: Nil
799804
else
800-
js.FieldDef(flags, encodeFieldSym(f), originalNameOfField(f), irTpe)
805+
val fieldIdent = encodeFieldSym(f)
806+
val originalName = originalNameOfField(f)
807+
val fieldDef = js.FieldDef(flags, fieldIdent, originalName, irTpe)
808+
val optionalStaticFieldGetter =
809+
if isStaticField then
810+
// Here we are generating a public static getter for the static field,
811+
// this is its API for other units. This is necessary for singleton
812+
// enum values, which are backed by static fields.
813+
val className = encodeClassName(classSym)
814+
val body = js.Block(
815+
js.LoadModule(className),
816+
js.SelectStatic(className, fieldIdent)(irTpe))
817+
js.MethodDef(js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic),
818+
encodeStaticMemberSym(f), originalName, Nil, irTpe,
819+
Some(body))(
820+
OptimizerHints.empty, None) :: Nil
821+
else
822+
Nil
823+
fieldDef :: optionalStaticFieldGetter
801824
}).toList
802825
}
803826

@@ -1433,7 +1456,7 @@ class JSCodeGen()(using genCtx: Context) {
14331456

14341457
case Assign(lhs0, rhs) =>
14351458
val sym = lhs0.symbol
1436-
if (sym.is(JavaStaticTerm))
1459+
if (sym.is(JavaStaticTerm) && sym.source != ctx.compilationUnit.source)
14371460
throw new FatalError(s"Assignment to static member ${sym.fullName} not supported")
14381461
def genRhs = genExpr(rhs)
14391462
val lhs = lhs0 match {
@@ -3899,8 +3922,15 @@ class JSCodeGen()(using genCtx: Context) {
38993922
39003923
(f, true)
39013924
} else*/ {
3902-
val f = js.Select(qual, encodeClassName(sym.owner),
3903-
encodeFieldSym(sym))(toIRType(sym.info))
3925+
val f =
3926+
val className = encodeClassName(sym.owner)
3927+
val fieldIdent = encodeFieldSym(sym)
3928+
val irType = toIRType(sym.info)
3929+
3930+
if sym.is(JavaStatic) then
3931+
js.SelectStatic(className, fieldIdent)(irType)
3932+
else
3933+
js.Select(qual, className, fieldIdent)(irType)
39043934

39053935
(f, false)
39063936
}

compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,25 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
9898
/** Return a list of forwarders for enum values defined in the companion object
9999
* for java interop.
100100
*/
101-
private def addedEnumForwarders(clazz: Symbol)(using Context): List[ValDef] = {
101+
private def addedEnumForwarders(clazz: Symbol)(using Context): List[MemberDef] = {
102102
val moduleCls = clazz.companionClass
103103
val moduleRef = ref(clazz.companionModule)
104104

105105
val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue))
106106
for { enumValue <- enums }
107107
yield {
108-
val fieldSym = newSymbol(clazz, enumValue.name.asTermName, EnumValue | JavaStatic, enumValue.info)
109-
fieldSym.addAnnotation(Annotations.Annotation(defn.ScalaStaticAnnot))
110-
ValDef(fieldSym, moduleRef.select(enumValue))
108+
def forwarderSym(flags: FlagSet, info: Type): Symbol { type ThisName = TermName } =
109+
val sym = newSymbol(clazz, enumValue.name.asTermName, flags, info)
110+
sym.addAnnotation(Annotations.Annotation(defn.ScalaStaticAnnot))
111+
sym
112+
val body = moduleRef.select(enumValue)
113+
if ctx.settings.scalajs.value then
114+
// Scala.js has no support for <clinit> so we must avoid assigning static fields in the enum class.
115+
// However, since the public contract for reading static fields in the IR ABI is to call "static getters",
116+
// we achieve the right contract with static forwarders instead.
117+
DefDef(forwarderSym(EnumValue | Method | JavaStatic, MethodType(Nil, enumValue.info)), body)
118+
else
119+
ValDef(forwarderSym(EnumValue | JavaStatic, enumValue.info), body)
111120
}
112121
}
113122

tests/neg-scalajs/js-enums.check

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Error: tests/neg-scalajs/js-enums.scala:4:5 -------------------------------------------------------------------------
2+
4 |enum MyEnum extends js.Object: // error
3+
|^
4+
|MyEnum extends scala.reflect.Enum which does not extend js.Any.
5+
5 | case Foo
6+
-- Error: tests/neg-scalajs/js-enums.scala:9:5 -------------------------------------------------------------------------
7+
7 |@js.native
8+
8 |@JSGlobal
9+
9 |enum MyEnumNative extends js.Object: // error
10+
|^
11+
|MyEnumNative extends scala.reflect.Enum which does not extend js.Any.
12+
10 | case Bar
13+
-- Error: tests/neg-scalajs/js-enums.scala:12:5 ------------------------------------------------------------------------
14+
12 |enum MyEnumAny extends js.Any: // error
15+
|^
16+
|Non-native JS classes and objects cannot directly extend AnyRef. They must extend a JS class (native or not).
17+
13 | case Foo
18+
-- Error: tests/neg-scalajs/js-enums.scala:17:5 ------------------------------------------------------------------------
19+
15 |@js.native
20+
16 |@JSGlobal
21+
17 |enum MyEnumNativeAny extends js.Any: // error
22+
|^
23+
|MyEnumNativeAny extends scala.reflect.Enum which does not extend js.Any.
24+
18 | case Bar

tests/neg-scalajs/js-enums.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import scala.scalajs.js
2+
import scala.scalajs.js.annotation._
3+
4+
enum MyEnum extends js.Object: // error
5+
case Foo
6+
7+
@js.native
8+
@JSGlobal
9+
enum MyEnumNative extends js.Object: // error
10+
case Bar
11+
12+
enum MyEnumAny extends js.Any: // error
13+
case Foo
14+
15+
@js.native
16+
@JSGlobal
17+
enum MyEnumNativeAny extends js.Any: // error
18+
case Bar
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package org.scalajs.testsuite.compiler
2+
3+
import org.junit.Assert._
4+
import org.junit.Test
5+
6+
class EnumTestScala3:
7+
import EnumTestScala3._
8+
9+
@Test def testColor1(): Unit =
10+
import EnumTestScala3.{Color1 => Color}
11+
12+
def code(c: Color): Character = c match
13+
case Color.Red => 'R'
14+
case Color.Green => 'G'
15+
case Color.Blue => 'B'
16+
17+
assert(Color.Red.ordinal == 0)
18+
assert(Color.Green.ordinal == 1)
19+
assert(Color.Blue.ordinal == 2)
20+
assert(Color.Red.productPrefix == "Red")
21+
assert(Color.Green.productPrefix == "Green")
22+
assert(Color.Blue.productPrefix == "Blue")
23+
assert(Color.valueOf("Red") == Color.Red)
24+
assert(Color.valueOf("Green") == Color.Green)
25+
assert(Color.valueOf("Blue") == Color.Blue)
26+
assert(Color.valueOf("Blue") != Color.Red)
27+
assert(Color.valueOf("Blue") != Color.Green)
28+
assert(Color.values(0) == Color.Red)
29+
assert(Color.values(1) == Color.Green)
30+
assert(Color.values(2) == Color.Blue)
31+
assert(code(Color.Red) == 'R')
32+
assert(code(Color.Green) == 'G')
33+
assert(code(Color.Blue) == 'B')
34+
35+
end testColor1
36+
37+
@Test def testColor2(): Unit = // copied from `color1`
38+
import EnumTestScala3.{Color2 => Color}
39+
40+
def code(c: Color): Character = c match
41+
case Color.Red => 'R'
42+
case Color.Green => 'G'
43+
case Color.Blue => 'B'
44+
45+
assert(Color.Red.ordinal == 0)
46+
assert(Color.Green.ordinal == 1)
47+
assert(Color.Blue.ordinal == 2)
48+
assert(Color.Red.productPrefix == "Red")
49+
assert(Color.Green.productPrefix == "Green")
50+
assert(Color.Blue.productPrefix == "Blue")
51+
assert(Color.valueOf("Red") == Color.Red)
52+
assert(Color.valueOf("Green") == Color.Green)
53+
assert(Color.valueOf("Blue") == Color.Blue)
54+
assert(Color.valueOf("Blue") != Color.Red)
55+
assert(Color.valueOf("Blue") != Color.Green)
56+
assert(Color.values(0) == Color.Red)
57+
assert(Color.values(1) == Color.Green)
58+
assert(Color.values(2) == Color.Blue)
59+
assert(code(Color.Red) == 'R')
60+
assert(code(Color.Green) == 'G')
61+
assert(code(Color.Blue) == 'B')
62+
63+
end testColor2
64+
65+
@Test def testCurrency1(): Unit =
66+
import EnumTestScala3.{Currency1 => Currency}
67+
68+
def code(c: Currency): String = c match
69+
case Currency.Dollar => "USD"
70+
case Currency.SwissFanc => "CHF"
71+
case Currency.Euro => "EUR"
72+
73+
assert(Currency.Dollar.ordinal == 0)
74+
assert(Currency.SwissFanc.ordinal == 1)
75+
assert(Currency.Euro.ordinal == 2)
76+
assert(Currency.Dollar.productPrefix == "Dollar")
77+
assert(Currency.SwissFanc.productPrefix == "SwissFanc")
78+
assert(Currency.Euro.productPrefix == "Euro")
79+
assert(Currency.valueOf("Dollar") == Currency.Dollar)
80+
assert(Currency.valueOf("SwissFanc") == Currency.SwissFanc)
81+
assert(Currency.valueOf("Euro") == Currency.Euro)
82+
assert(Currency.valueOf("Euro") != Currency.Dollar)
83+
assert(Currency.valueOf("Euro") != Currency.SwissFanc)
84+
assert(Currency.values(0) == Currency.Dollar)
85+
assert(Currency.values(1) == Currency.SwissFanc)
86+
assert(Currency.values(2) == Currency.Euro)
87+
assert(Currency.Dollar.dollarValue == 1.00)
88+
assert(Currency.SwissFanc.dollarValue == 1.09)
89+
assert(Currency.Euro.dollarValue == 1.18)
90+
assert(code(Currency.Dollar) == "USD")
91+
assert(code(Currency.SwissFanc) == "CHF")
92+
assert(code(Currency.Euro) == "EUR")
93+
94+
95+
end testCurrency1
96+
97+
@Test def testCurrency2(): Unit = // copied from `testCurrency1`
98+
import EnumTestScala3.{Currency2 => Currency}
99+
100+
def code(c: Currency): String = c match
101+
case Currency.Dollar => "USD"
102+
case Currency.SwissFanc => "CHF"
103+
case Currency.Euro => "EUR"
104+
105+
assert(Currency.Dollar.ordinal == 0)
106+
assert(Currency.SwissFanc.ordinal == 1)
107+
assert(Currency.Euro.ordinal == 2)
108+
assert(Currency.Dollar.productPrefix == "Dollar")
109+
assert(Currency.SwissFanc.productPrefix == "SwissFanc")
110+
assert(Currency.Euro.productPrefix == "Euro")
111+
assert(Currency.valueOf("Dollar") == Currency.Dollar)
112+
assert(Currency.valueOf("SwissFanc") == Currency.SwissFanc)
113+
assert(Currency.valueOf("Euro") == Currency.Euro)
114+
assert(Currency.valueOf("Euro") != Currency.Dollar)
115+
assert(Currency.valueOf("Euro") != Currency.SwissFanc)
116+
assert(Currency.values(0) == Currency.Dollar)
117+
assert(Currency.values(1) == Currency.SwissFanc)
118+
assert(Currency.values(2) == Currency.Euro)
119+
assert(Currency.Dollar.dollarValue == 1.00)
120+
assert(Currency.SwissFanc.dollarValue == 1.09)
121+
assert(Currency.Euro.dollarValue == 1.18)
122+
assert(code(Currency.Dollar) == "USD")
123+
assert(code(Currency.SwissFanc) == "CHF")
124+
assert(code(Currency.Euro) == "EUR")
125+
126+
end testCurrency2
127+
128+
@Test def testOpt(): Unit =
129+
130+
def encode[T <: AnyVal](t: Opt[T]): T | Null = t match
131+
case Opt.Sm(t) => t
132+
case Opt.Nn => null
133+
134+
assert(Opt.Sm(1).ordinal == 0)
135+
assert(Opt.Nn.ordinal == 1)
136+
assert(Opt.Sm(1).productPrefix == "Sm")
137+
assert(Opt.Nn.productPrefix == "Nn")
138+
assert(Opt.valueOf("Nn") == Opt.Nn)
139+
assert(Opt.values(0) == Opt.Nn)
140+
assert(Opt.Sm("hello").value == "hello")
141+
assert(encode(Opt.Sm(23)) == 23)
142+
assert(encode(Opt.Nn) == null)
143+
144+
end testOpt
145+
146+
object EnumTestScala3:
147+
148+
enum Color1 derives Eql:
149+
case Red, Green, Blue
150+
151+
enum Color2 extends java.lang.Enum[Color2] derives Eql:
152+
case Red, Green, Blue
153+
154+
// test "non-simple" cases with anonymous subclasses
155+
enum Currency1(val dollarValue: Double) derives Eql:
156+
case Dollar extends Currency1(1.0)
157+
case SwissFanc extends Currency1(1.09)
158+
case Euro extends Currency1(1.18)
159+
160+
enum Currency2(val dollarValue: Double) extends java.lang.Enum[Currency2] derives Eql:
161+
case Dollar extends Currency2(1.0)
162+
case SwissFanc extends Currency2(1.09)
163+
case Euro extends Currency2(1.18)
164+
165+
enum Opt[+T]:
166+
case Sm[+T1](value: T1) extends Opt[T1]
167+
case Nn extends Opt[Nothing]
168+
169+
end EnumTestScala3

0 commit comments

Comments
 (0)