diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 8d31916b7401..281591b7d16a 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -773,7 +773,7 @@ class JSCodeGen()(using genCtx: Context) { // Generate the fields of a class ------------------------------------------ /** Gen definitions for the fields of a class. */ - private def genClassFields(td: TypeDef): List[js.AnyFieldDef] = { + private def genClassFields(td: TypeDef): List[js.MemberDef] = { val classSym = td.symbol.asClass assert(currentClassSym.get == classSym, "genClassFields called with a ClassDef other than the current one") @@ -785,19 +785,42 @@ class JSCodeGen()(using genCtx: Context) { !f.isOneOf(Method | Module) && f.isTerm && !f.hasAnnotation(jsdefn.JSNativeAnnot) && !f.hasAnnotation(jsdefn.JSOptionalAnnot) - }.map({ f => + }.flatMap({ f => implicit val pos = f.span - val flags = js.MemberFlags.empty.withMutable(f.is(Mutable)) + val isStaticField = f.is(JavaStatic).ensuring(isStatic => !(isStatic && isJSClass)) + + val namespace = if isStaticField then js.MemberNamespace.PublicStatic else js.MemberNamespace.Public + val mutable = isStaticField || f.is(Mutable) + + val flags = js.MemberFlags.empty.withMutable(mutable).withNamespace(namespace) val irTpe = if (isJSClass) genExposedFieldIRType(f) else toIRType(f.info) if (isJSClass && f.isJSExposed) - js.JSFieldDef(flags, genExpr(f.jsName)(f.sourcePos), irTpe) + js.JSFieldDef(flags, genExpr(f.jsName)(f.sourcePos), irTpe) :: Nil else - js.FieldDef(flags, encodeFieldSym(f), originalNameOfField(f), irTpe) + val fieldIdent = encodeFieldSym(f) + val originalName = originalNameOfField(f) + val fieldDef = js.FieldDef(flags, fieldIdent, originalName, irTpe) + val optionalStaticFieldGetter = + if isStaticField then + // Here we are generating a public static getter for the static field, + // this is its API for other units. This is necessary for singleton + // enum values, which are backed by static fields. + val className = encodeClassName(classSym) + val body = js.Block( + js.LoadModule(className), + js.SelectStatic(className, fieldIdent)(irTpe)) + js.MethodDef(js.MemberFlags.empty.withNamespace(js.MemberNamespace.PublicStatic), + encodeStaticMemberSym(f), originalName, Nil, irTpe, + Some(body))( + OptimizerHints.empty, None) :: Nil + else + Nil + fieldDef :: optionalStaticFieldGetter }).toList } @@ -1433,7 +1456,7 @@ class JSCodeGen()(using genCtx: Context) { case Assign(lhs0, rhs) => val sym = lhs0.symbol - if (sym.is(JavaStaticTerm)) + if (sym.is(JavaStaticTerm) && sym.source != ctx.compilationUnit.source) throw new FatalError(s"Assignment to static member ${sym.fullName} not supported") def genRhs = genExpr(rhs) val lhs = lhs0 match { @@ -3899,8 +3922,15 @@ class JSCodeGen()(using genCtx: Context) { (f, true) } else*/ { - val f = js.Select(qual, encodeClassName(sym.owner), - encodeFieldSym(sym))(toIRType(sym.info)) + val f = + val className = encodeClassName(sym.owner) + val fieldIdent = encodeFieldSym(sym) + val irType = toIRType(sym.info) + + if sym.is(JavaStatic) then + js.SelectStatic(className, fieldIdent)(irType) + else + js.Select(qual, className, fieldIdent)(irType) (f, false) } diff --git a/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala b/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala index d308cea9f6d7..9a7665d2286e 100644 --- a/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala +++ b/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala @@ -98,16 +98,25 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase => /** Return a list of forwarders for enum values defined in the companion object * for java interop. */ - private def addedEnumForwarders(clazz: Symbol)(using Context): List[ValDef] = { + private def addedEnumForwarders(clazz: Symbol)(using Context): List[MemberDef] = { val moduleCls = clazz.companionClass val moduleRef = ref(clazz.companionModule) val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue)) for { enumValue <- enums } yield { - val fieldSym = newSymbol(clazz, enumValue.name.asTermName, EnumValue | JavaStatic, enumValue.info) - fieldSym.addAnnotation(Annotations.Annotation(defn.ScalaStaticAnnot)) - ValDef(fieldSym, moduleRef.select(enumValue)) + def forwarderSym(flags: FlagSet, info: Type): Symbol { type ThisName = TermName } = + val sym = newSymbol(clazz, enumValue.name.asTermName, flags, info) + sym.addAnnotation(Annotations.Annotation(defn.ScalaStaticAnnot)) + sym + val body = moduleRef.select(enumValue) + if ctx.settings.scalajs.value then + // Scala.js has no support for so we must avoid assigning static fields in the enum class. + // However, since the public contract for reading static fields in the IR ABI is to call "static getters", + // we achieve the right contract with static forwarders instead. + DefDef(forwarderSym(EnumValue | Method | JavaStatic, MethodType(Nil, enumValue.info)), body) + else + ValDef(forwarderSym(EnumValue | JavaStatic, enumValue.info), body) } } diff --git a/tests/neg-scalajs/js-enums.check b/tests/neg-scalajs/js-enums.check new file mode 100644 index 000000000000..eb358f16e57a --- /dev/null +++ b/tests/neg-scalajs/js-enums.check @@ -0,0 +1,24 @@ +-- Error: tests/neg-scalajs/js-enums.scala:4:5 ------------------------------------------------------------------------- +4 |enum MyEnum extends js.Object: // error + |^ + |MyEnum extends scala.reflect.Enum which does not extend js.Any. +5 | case Foo +-- Error: tests/neg-scalajs/js-enums.scala:9:5 ------------------------------------------------------------------------- +7 |@js.native +8 |@JSGlobal +9 |enum MyEnumNative extends js.Object: // error + |^ + |MyEnumNative extends scala.reflect.Enum which does not extend js.Any. +10 | case Bar +-- Error: tests/neg-scalajs/js-enums.scala:12:5 ------------------------------------------------------------------------ +12 |enum MyEnumAny extends js.Any: // error + |^ + |Non-native JS classes and objects cannot directly extend AnyRef. They must extend a JS class (native or not). +13 | case Foo +-- Error: tests/neg-scalajs/js-enums.scala:17:5 ------------------------------------------------------------------------ +15 |@js.native +16 |@JSGlobal +17 |enum MyEnumNativeAny extends js.Any: // error + |^ + |MyEnumNativeAny extends scala.reflect.Enum which does not extend js.Any. +18 | case Bar diff --git a/tests/neg-scalajs/js-enums.scala b/tests/neg-scalajs/js-enums.scala new file mode 100644 index 000000000000..cf94e690011b --- /dev/null +++ b/tests/neg-scalajs/js-enums.scala @@ -0,0 +1,18 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +enum MyEnum extends js.Object: // error + case Foo + +@js.native +@JSGlobal +enum MyEnumNative extends js.Object: // error + case Bar + +enum MyEnumAny extends js.Any: // error + case Foo + +@js.native +@JSGlobal +enum MyEnumNativeAny extends js.Any: // error + case Bar diff --git a/tests/sjs-junit/test/org/scalajs/testsuite/compiler/EnumTestScala3.scala b/tests/sjs-junit/test/org/scalajs/testsuite/compiler/EnumTestScala3.scala new file mode 100644 index 000000000000..be7dae4d7af6 --- /dev/null +++ b/tests/sjs-junit/test/org/scalajs/testsuite/compiler/EnumTestScala3.scala @@ -0,0 +1,169 @@ +package org.scalajs.testsuite.compiler + +import org.junit.Assert._ +import org.junit.Test + +class EnumTestScala3: + import EnumTestScala3._ + + @Test def testColor1(): Unit = + import EnumTestScala3.{Color1 => Color} + + def code(c: Color): Character = c match + case Color.Red => 'R' + case Color.Green => 'G' + case Color.Blue => 'B' + + assert(Color.Red.ordinal == 0) + assert(Color.Green.ordinal == 1) + assert(Color.Blue.ordinal == 2) + assert(Color.Red.productPrefix == "Red") + assert(Color.Green.productPrefix == "Green") + assert(Color.Blue.productPrefix == "Blue") + assert(Color.valueOf("Red") == Color.Red) + assert(Color.valueOf("Green") == Color.Green) + assert(Color.valueOf("Blue") == Color.Blue) + assert(Color.valueOf("Blue") != Color.Red) + assert(Color.valueOf("Blue") != Color.Green) + assert(Color.values(0) == Color.Red) + assert(Color.values(1) == Color.Green) + assert(Color.values(2) == Color.Blue) + assert(code(Color.Red) == 'R') + assert(code(Color.Green) == 'G') + assert(code(Color.Blue) == 'B') + + end testColor1 + + @Test def testColor2(): Unit = // copied from `color1` + import EnumTestScala3.{Color2 => Color} + + def code(c: Color): Character = c match + case Color.Red => 'R' + case Color.Green => 'G' + case Color.Blue => 'B' + + assert(Color.Red.ordinal == 0) + assert(Color.Green.ordinal == 1) + assert(Color.Blue.ordinal == 2) + assert(Color.Red.productPrefix == "Red") + assert(Color.Green.productPrefix == "Green") + assert(Color.Blue.productPrefix == "Blue") + assert(Color.valueOf("Red") == Color.Red) + assert(Color.valueOf("Green") == Color.Green) + assert(Color.valueOf("Blue") == Color.Blue) + assert(Color.valueOf("Blue") != Color.Red) + assert(Color.valueOf("Blue") != Color.Green) + assert(Color.values(0) == Color.Red) + assert(Color.values(1) == Color.Green) + assert(Color.values(2) == Color.Blue) + assert(code(Color.Red) == 'R') + assert(code(Color.Green) == 'G') + assert(code(Color.Blue) == 'B') + + end testColor2 + + @Test def testCurrency1(): Unit = + import EnumTestScala3.{Currency1 => Currency} + + def code(c: Currency): String = c match + case Currency.Dollar => "USD" + case Currency.SwissFanc => "CHF" + case Currency.Euro => "EUR" + + assert(Currency.Dollar.ordinal == 0) + assert(Currency.SwissFanc.ordinal == 1) + assert(Currency.Euro.ordinal == 2) + assert(Currency.Dollar.productPrefix == "Dollar") + assert(Currency.SwissFanc.productPrefix == "SwissFanc") + assert(Currency.Euro.productPrefix == "Euro") + assert(Currency.valueOf("Dollar") == Currency.Dollar) + assert(Currency.valueOf("SwissFanc") == Currency.SwissFanc) + assert(Currency.valueOf("Euro") == Currency.Euro) + assert(Currency.valueOf("Euro") != Currency.Dollar) + assert(Currency.valueOf("Euro") != Currency.SwissFanc) + assert(Currency.values(0) == Currency.Dollar) + assert(Currency.values(1) == Currency.SwissFanc) + assert(Currency.values(2) == Currency.Euro) + assert(Currency.Dollar.dollarValue == 1.00) + assert(Currency.SwissFanc.dollarValue == 1.09) + assert(Currency.Euro.dollarValue == 1.18) + assert(code(Currency.Dollar) == "USD") + assert(code(Currency.SwissFanc) == "CHF") + assert(code(Currency.Euro) == "EUR") + + + end testCurrency1 + + @Test def testCurrency2(): Unit = // copied from `testCurrency1` + import EnumTestScala3.{Currency2 => Currency} + + def code(c: Currency): String = c match + case Currency.Dollar => "USD" + case Currency.SwissFanc => "CHF" + case Currency.Euro => "EUR" + + assert(Currency.Dollar.ordinal == 0) + assert(Currency.SwissFanc.ordinal == 1) + assert(Currency.Euro.ordinal == 2) + assert(Currency.Dollar.productPrefix == "Dollar") + assert(Currency.SwissFanc.productPrefix == "SwissFanc") + assert(Currency.Euro.productPrefix == "Euro") + assert(Currency.valueOf("Dollar") == Currency.Dollar) + assert(Currency.valueOf("SwissFanc") == Currency.SwissFanc) + assert(Currency.valueOf("Euro") == Currency.Euro) + assert(Currency.valueOf("Euro") != Currency.Dollar) + assert(Currency.valueOf("Euro") != Currency.SwissFanc) + assert(Currency.values(0) == Currency.Dollar) + assert(Currency.values(1) == Currency.SwissFanc) + assert(Currency.values(2) == Currency.Euro) + assert(Currency.Dollar.dollarValue == 1.00) + assert(Currency.SwissFanc.dollarValue == 1.09) + assert(Currency.Euro.dollarValue == 1.18) + assert(code(Currency.Dollar) == "USD") + assert(code(Currency.SwissFanc) == "CHF") + assert(code(Currency.Euro) == "EUR") + + end testCurrency2 + + @Test def testOpt(): Unit = + + def encode[T <: AnyVal](t: Opt[T]): T | Null = t match + case Opt.Sm(t) => t + case Opt.Nn => null + + assert(Opt.Sm(1).ordinal == 0) + assert(Opt.Nn.ordinal == 1) + assert(Opt.Sm(1).productPrefix == "Sm") + assert(Opt.Nn.productPrefix == "Nn") + assert(Opt.valueOf("Nn") == Opt.Nn) + assert(Opt.values(0) == Opt.Nn) + assert(Opt.Sm("hello").value == "hello") + assert(encode(Opt.Sm(23)) == 23) + assert(encode(Opt.Nn) == null) + + end testOpt + +object EnumTestScala3: + + enum Color1 derives Eql: + case Red, Green, Blue + + enum Color2 extends java.lang.Enum[Color2] derives Eql: + case Red, Green, Blue + + // test "non-simple" cases with anonymous subclasses + enum Currency1(val dollarValue: Double) derives Eql: + case Dollar extends Currency1(1.0) + case SwissFanc extends Currency1(1.09) + case Euro extends Currency1(1.18) + + enum Currency2(val dollarValue: Double) extends java.lang.Enum[Currency2] derives Eql: + case Dollar extends Currency2(1.0) + case SwissFanc extends Currency2(1.09) + case Euro extends Currency2(1.18) + + enum Opt[+T]: + case Sm[+T1](value: T1) extends Opt[T1] + case Nn extends Opt[Nothing] + +end EnumTestScala3