Skip to content

Conversation

@tanishiking
Copy link
Member

@tanishiking tanishiking commented Nov 7, 2025

Fixes #12637

Previously, Java enum forwarders were initialized to null when the companion's static initializer was triggered before the Java enum forwarder class's static initializer.

For example, when enum Testme extends java.lang.Enum[Testme] is accessed from Scala (which accesses Testme$.Hello):

  • Testme$.<clinit> is triggered
  • The static initializer creates enum values by calling Testme$.new(...)
  • It constructs Testme$$anon$1 (which represents Hello), a subtype of Testme
  • Therefore, Testme.<clinit> is triggered
  • Testme.<clinit> tries to initialize its Testme.Hello field by pulling from Testme$.Hello
  • However, it's still null during the companion's static initialization!

See: #12637 (comment)

// Testme.scala
object TestenumS:
    def go() = println("Scala: Testme Hello= " + Testme.Hello)

enum Testme extends java.lang.Enum[Testme]:
    case Hello

// TestenumJ.java
public class TestenumJ {
    public static void main(String[] args) {
        TestenumS.go();
        System.out.println("Java: Testme Hello= " + Testme.Hello);
    }
}
509000017-f0bb265c-42cb-44a4-bb64-c7cedebcf1e7

full javap result is available here: https://github.com/tanishiking/kitchensink/tree/main/scala3/12637

This commit fixes the initialization problem by having the companion object's static initializer push enum values to the forwarders after it finishes initializing the enum value fields.

When the companion is accessed first:

  • Companion's <clinit> runs and creates enum values
  • During initialization, the forwarder's <clinit> is triggered
  • Forwarders pull from the companion (value will be null)
  • Companion's <clinit> pushes final values to forwarders at the end

When the forwarder is accessed first:

  • Enum class's <clinit> tries to initialize the forwarder via getstatic from the companion
  • This triggers the companion's <clinit> first
  • Companion's <clinit> pushes values to the forwarders
  • The original putstatic completes (resulting in double assignment, but with the correct value)

Drawbacks:

  • We assign the forwarder field twice, making it slightly slower than before
  • We changed the Java enum forwarder fields to be non-final

Now it's possible to update the static forwarder like

public class Main {
    public static void main(String[] args) {
        TestenumS.go();
        System.out.println("Java: Testme Hello= " + Testme.Hello); // prints Java: Testme Hello= Hello
        Testme.Hello = null;
        System.out.println("Java: Testme Hello= " + Testme.Hello); // prints Java: Testme Hello= null
    }
}

If making the Java enum forwarder non-final isn't acceptable, other option would be generating a proxy method like for Scala.js, but Java will need to call Testme.Hello(), instead of Testme.Hello.

Fixes scala#12637

Previously, Java enum forwarders were initialized to null when the companion's static initializer was triggered before the Java enum forwarder class's static initializer.

For example, when `enum Testme extends java.lang.Enum[Testme]` is accessed from Scala (which accesses `Testme$.Hello`):

- `Testme$.<clinit>` is triggered
- The static initializer creates enum values by calling `Testme$.new(...)`
- It constructs `Testme$$anon$1` (which represents `Hello`), a subtype of `Testme`
- Therefore, `Testme.<clinit>` is triggered
- `Testme.<clinit>` tries to initialize its `Testme.Hello` field by pulling from `Testme$.Hello`
- However, it's still null during the companion's static initialization!

See: scala#12637 (comment)

```scala
// Testme.scala
object TestenumS:
    def go() = println("Scala: Testme Hello= " + Testme.Hello)

enum Testme extends java.lang.Enum[Testme]:
    case Hello

// TestenumJ.java
public class TestenumJ {
    public static void main(String[] args) {
        TestenumS.go();
        System.out.println("Java: Testme Hello= " + Testme.Hello);
    }
}
```

This commit fixes the initialization problem by having the companion object's static initializer push enum values to the forwarders after it finishes initializing the enum value fields.

**When the companion is accessed first:**
- Companion's `<clinit>` runs and creates enum values
- During initialization, the forwarder's `<clinit>` is triggered
- Forwarders pull from the companion (value will be null)
- Companion's `<clinit>` pushes final values to forwarders at the end

**When the forwarder is accessed first:**
- Enum class's `<clinit>` tries to initialize the forwarder via `getstatic` from the companion
- This triggers the companion's `<clinit>` first
- Companion's `<clinit>` pushes values to the forwarders
- The original `putstatic` completes (resulting in double assignment, but with the correct value)

**Drawbacks:**
- We assign the forwarder field twice, making it slightly slower than before
- **We changed the Java enum forwarder fields to be non-final**
@tanishiking tanishiking marked this pull request as ready for review November 7, 2025 11:01
@Gedochao Gedochao requested a review from sjrd November 7, 2025 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enum (extends java) => null in java, when scala looks at it before java

2 participants