Skip to content

Fix #9809: enable static fields in Scala.js and change forwarders #9955

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

Merged
merged 6 commits into from
Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
17 changes: 13 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 <clinit> 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)
}
}

Expand Down
24 changes: 24 additions & 0 deletions tests/neg-scalajs/js-enums.check
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions tests/neg-scalajs/js-enums.scala
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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