Skip to content

Sum-subtypes handling #47

Description

@LDemetrios

Hi!

I find incredible your work on mapping rust concepts to jvm concepts, however, I have a proposal. In Rust it's a common practice to have different subsets of a larger sum types, expressed as different enums. Let's say, a Rust programmer wants to write types A | B | C and A | B, both equally meaningful in their program. There are two common ways to do it:

enum AorBorC { VarA(A), VarB(B), VarC(C) }
enum AorB { VarA(A), VarB(B) }

or

enum AorBorC { VarAB(AorB), VarC(C) }
enum AorB { VarA(A), VarB(B) }

, both provide certain benefits:

  • The former can be easily extended with another type like A | C if needed, and (since names are usually aren't as transparent as AorB) it gives more clarity as to what the options of AorBorC are,
  • The latter doesn't require writing additional From and Into impls, and enables more concise matching patterns at times.

However, what it currently compiles to is nothing like what it would be reasonably represented with in JVM languages. The typical solution there would be

sealed interface AorBorC permits AorB, VarC {}
sealed interface AorB extends AorBorC permits VarA, VarB {}
record VarA(A a) implements AorB {}
record VarB(B b) implements AorB {}
record VarC(C c) implements AorBorC {}

or better yet, have A, B and C directly implementing AorBorC and AorB where needed.

I understand the latter might be a stretch, especially considering the fact that A might as well be i32 and there's no putting custom interfaces on Integer. Unless you do some AspectJ-style weaving on stdlib jar, of course, which is considered to be a dangerous spell even by the most fearless siths.

In my head, here's how this might look in code. An attribute macro signifying that this particular enum should be a sealed interface, not a class. Still, having sealed class might be preferable for indirection reasons: INVOKEINTERFACE is slower than INVOKEVIRTUAL, although I'm not sure that applies to sealed hierarchies; that's why I'm saying an interface should be an option and not default. Consequently, one-argument variants of said "interface" enum might just be done as implementing an interface rather than creating a new wrapper class. That is, if the enum and the variant parameter are in the same... module? whatever compiles into one package.

I think that's it, although I'm all for brainstorming more ideas if you feel like discussing this. There a lot of crazy ideas that I have thought of for this project...

P.S. I am absolutely stunned by your project, seriously. Just this morning I thought about what dark magic I could pull off to make cross-FFI async between Kotlin's suspend and Rust's async. I couldn't express enough how impressed I am that someone has not only thought about it, but already done it. I hope this project brings you joy

Activity

  1. IntegralPilot commented on Aug 26, 2026

    @IntegralPilot
    Owner

    Thank you so much for your kind words and suggestion!

    I agree the current enum ABI (with the abstract class etc.) wasn't great, and I actually designed it early on over a year ago when I was first working on this, and didn't put that much thought into it. Looking back on it now I would love to make it better.

    I think your suggestion is really great, and over today and yesterday I've been refactoring how enums work to work to implement it. I just pushed my first prototype of it here: 6ef58b4

    The main differences to what you suggested are:

    • I didn't make the interfaces sealed because I didn't want to break Java 8 compatibility. If you think it's important, I can start implementing initial MRJAR support (which I'll need anyway for when I want to support Valhalla support) so it's used where it can.
    • I made the elimination of the wrapped object opt-in, because sometimes it wouldn't be wanted and unexpectedly doing it wouldn't be ideal in my opinion (though if you think it would be better opt-out, I'd be happy to do that instead too, I'm not too accustomed to what JVM language devs would expect). In my current system, you need to mark a variant with #[jvm::subtype], and then constructors emit a simple cast instead of allocating an intermediary wrapper (Root$Leaf).

    As an example of how the current system works (feedback to make it better much appreciated!):

    pub enum Leaf {
        A(i32),
        B,
    }
    
    pub enum Root {
        Leaf(Leaf),
        Other(i32),
    }

    is

    public interface Leaf {
        static int variantIndex(Leaf value) { /* ... */ }
        static boolean eq(Leaf a, Leaf b) { /* ... */ }
    
        final class A implements Leaf {
            public int field0;
            public A(int field0) { this.field0 = field0; }
        }
    
        final class B implements Leaf {
            public B() {}
        }
    }
    
    public interface Root {
        static int variantIndex(Root value) { /* ... */ }
        static boolean eq(Root a, Root b) { /* ... */ }
    
        // Explicit wrapper class holding the Leaf payload
        final class Leaf implements Root {
            public my_crate.Leaf field0;
            public Leaf(my_crate.Leaf field0) { this.field0 = field0; }
        }
    
        final class Other implements Root {
            public int field0;
            public Other(int field0) { this.field0 = field0; }
        }
    }

    and you use it like

    Leaf leaf = new Leaf.A(42);
    Root root = new Root.Leaf(leaf); // Requires allocating a wrapper object
    int payload = ((Leaf.A) ((Root.Leaf) root).field0).field0;

    And then, if you use jvm::subtype, like:

    #![feature(register_tool)]
    #![register_tool(jvm)]
    
    pub enum Leaf {
        A(i32),
        B,
    }
    
    pub enum Root {
        #[jvm::subtype]
        Leaf(Leaf),
        Other(i32),
    }

    it becomes

    public interface Root {
        static int variantIndex(Root value) {
            if (value instanceof Leaf) return 0;       // Tag for Root::Leaf
            if (value instanceof Root.Other) return 1; // Tag for Root::Other
            return -1;
        }
    
        static boolean eq(Root left, Root right) {
            // [...] 
            // Handles structural equality across both direct and subtype variants
        }
    
        final class Other implements Root {
            public int field0;
            public Other(int field0) { this.field0 = field0; }
        }
    }
    
    public interface Leaf extends Root {
        static int variantIndex(Leaf value) { /* 0 for A, 1 for B */ }
        static boolean eq(Leaf a, Leaf b) { /* ... */ }
    
        final class A implements Leaf { // Transitively implements Root
            public int field0;
            public A(int field0) { this.field0 = field0; }
        }
    
        final class B implements Leaf { // Transitively implements Root
            public B() {}
        }
    }

    and you use it like

    Leaf leaf = new Leaf.A(42);
    Root root = leaf;
    
    int outerVariant = Root.variantIndex(root); // 0 (Root::Leaf)
    int payload = ((Leaf.A) root).field0;        // Direct access: 42
    boolean equal = Root.eq(root, new Leaf.A(42));

    Let me know if that's good or if you'd like it better in any way!

    I really value feedback from those experienced in JVM languages (which I aren't as much myself) to help make it as natural and easy to use as possible (which is something I really want with this project), and if you have any more I'd so appreciate any additional issues if you have time!

    Thank you so much again! :)

  2. IntegralPilot commented on Aug 27, 2026

    @IntegralPilot
    Owner

    Oh btw you mentioned "cross-FFI async between Kotlin's suspend and Rust's async". Actually that currently isnt supported yet (individually, 1. using async and coroutines in Rust, and 2. FFI Rust<->JVM, are implemented, but not together) but I actually think it would be something really cool to have! What were your thoughts in that area about the best way?

  3. LDemetrios commented on Aug 29, 2026

    @LDemetrios
    Author

    Oh, thank you for the fast response!
    This is incredible work. Well, in close future I'm going to feed a huge Rust project to your compiler, and see what happens xD. Maybe I'll have more to say then. Right now as a concept it looks powerful enough already.

    As a matter of fact, I do find sealedness of the hierarchy important (it improves both compiler checks and JIT optimizations), but compatibility is a strong argument. So, I think I'll just postprocess the generated jar, putting sealed modifiers where needed xD.

    Well, I do consider myself experienced in JVM langs, but in Rust on the other hand -- not at all. It took me some researching to figure out what Rust usually does with async functions, and it seems that they are essentialy compiled into functions that return Futures. And all the state handling happens inside those Futures. Kotlin does it another way: each suspend function after compilation get a new argument, Continuation, and the body of the function is transformed into state machine handling different possible continuations. When the function is suspended, the special marker is returned, and if the function returned normally -- just the return value. I think Kotlin does that to preserve JVM stacktraces, and also because nesting futures might lead to a lot of hard-to-debug indirections. I'm currently not sure what to make of this, perhaps there could be some functions like suspend fun <T> RustFuture<T>.await() : T and fun <T> futureOf(body: suspend () -> T) : RustFuture<T>. This might require further investigation, because there isn't a standard JVM way to do async, unlike with other things.

  4. IntegralPilot commented on Aug 31, 2026

    @IntegralPilot
    Owner

    Thank you! I actually started working on async interop when I sent my comment 4 days ago, and I landed on a similar approach, and I've just pushed it now, as I think it's ready. You can see examples of how it interacts with Kotlin in https://github.com/IntegralPilot/rustc_codegen_jvm/tree/main/tests/kotlin, is that idiomatic and nice for Kotlin developers, or do you think I could make it better?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions