Skip to content

Fix Java enum forwarders to pull from companion on initialization #24361

Merged
tanishiking merged 3 commits intoscala:mainfrom
tanishiking:enum-java-null
Feb 19, 2026
Merged

Fix Java enum forwarders to pull from companion on initialization #24361
tanishiking merged 3 commits intoscala:mainfrom
tanishiking:enum-java-null

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.

Or maybe make Scala access Java forwarder on Testme.Hello if the forwarder exists? Now sure it's plausible.

@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
@tgodzik
Copy link
Contributor

tgodzik commented Nov 13, 2025

We discussed this during the core meeting and we are ok with taking with this approach.

@liosedhel
Copy link

Hi ;) Any news when this might be merged?

@Gedochao
Copy link
Contributor

It seems it got stuck on review.
bump @sjrd


// Store forwarder symbols for later use in companion initialization
if forwarderSyms.nonEmpty then
enumForwarders(clazz) = forwarderSyms.toList
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking nonEmpty is just a minor optimization, right?

@tgodzik
Copy link
Contributor

tgodzik commented Feb 18, 2026

@tanishiking I guess we only need to rebase and we should be good to merge

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
Copy link
Member Author

Thanks, rebased on main without conflicts, let's see CI and will merge it :)

@tanishiking tanishiking merged commit 56c2020 into scala:main Feb 19, 2026
60 checks passed
@tanishiking tanishiking deleted the enum-java-null branch February 19, 2026 03:25
@Gedochao Gedochao added the backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. label Feb 19, 2026
@Gedochao Gedochao added this to the 3.8.3 milestone Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it.

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

6 participants

Comments