Skip to content

java2swift: Add Support for Enum Cases #76

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 7 commits into from
Oct 20, 2024

Conversation

jrosen081
Copy link
Contributor

@jrosen081 jrosen081 commented Oct 14, 2024

Closes #21
Adds a few nice pieces of support for enums:

  1. Adds static properties on the main struct so we can have the static member dot syntax (i.e. .APRIL for april static property on Month).

There are a few things we could improve here related to the static properties that I'm interested to see if we should update:

  1. Update the SNAKE_CASE static properties to lower camel case to be more swifty
  2. Right now to get the environment we have a try! which is likely not great. If the programmer already is using the java-swift work, they likely have the JVM all set up, so this should be fine, but if not, we might want to remove the static properties or come up with another way of creating the environment statically that is not failing (if that is even possible)
  3. We also need to add a force unwrap here since the java field won't compile with a non-optional value

Open Question the I'm unsure of:

  • Should we add an Equatable conformance for these using the equals function, so that we can get a good implementation of ~= for switch statements?
  • Should we add an ~= implementation itself (possibly also using equals)?

@ktoso ktoso changed the title Add Support for Enum Cases java2swift: Add Support for Enum Cases Oct 15, 2024
@ktoso ktoso requested a review from DougGregor October 15, 2024 08:13
return nil

if field.isEnumConstant() {
return "public static let \(raw: field.getName()) = try! JavaClass<Self>(in: JavaVirtualMachine.shared().environment()).\(raw: field.getName())!"
Copy link
Member

Choose a reason for hiding this comment

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

If we've going to use JavaVirtualMachine here, we need to also make sure to import JavaKitVM in the generated code. There's a set of imports in the JavaTranslator that you would need to add to.

I do worry a bit about calling JavaVirtualMachine.shared() here in something that looks like a constant, and I wonder if we should consider alternative ways to model Java enums.

@DougGregor
Copy link
Member

@jrosen081 your items (2) and (3) have me wondering if we should try different ways of bringing Java enum constants into Swift. For example, we could collect all of the enum cases and put them into a synthesized Swift enum, e.g.,

/* assuming this is java.time.Month */
extension Month {
  public enum EnumConstant {
    case JANUARY
    case FEBRUARY
    // ...
  }

  public init(_ constant: EnumConstant, environment: JNIEnvironment) { ... }

  public var enumConstant: EnumConstant { get }
}

Then we could add matching operators for switch and such as needed. Perhaps even factor some of these operations out into a protocol to which Java enum classes would be made to conform. We would no longer require the virtual machine instance nor the try!.

What do you think? I'm open to other ideas, too!

@jrosen081
Copy link
Contributor Author

@jrosen081 your items (2) and (3) have me wondering if we should try different ways of bringing Java enum constants into Swift. For example, we could collect all of the enum cases and put them into a synthesized Swift enum, e.g.,

/* assuming this is java.time.Month */
extension Month {
  public enum EnumConstant {
    case JANUARY
    case FEBRUARY
    // ...
  }

  public init(_ constant: EnumConstant, environment: JNIEnvironment) { ... }

  public var enumConstant: EnumConstant { get }
}

Then we could add matching operators for switch and such as needed. Perhaps even factor some of these operations out into a protocol to which Java enum classes would be made to conform. We would no longer require the virtual machine instance nor the try!.

What do you think? I'm open to other ideas, too!

I like it. I think we would need EnumConstant to be computed since we might get the value from Java, which would not have a way of creating the synthesized enum in Swift (I don't think). I'll prototype this setup which gives us the benefit of Swift enums without needing to use the environment and force unwrap everywhere

@jrosen081
Copy link
Contributor Author

@jrosen081 your items (2) and (3) have me wondering if we should try different ways of bringing Java enum constants into Swift. For example, we could collect all of the enum cases and put them into a synthesized Swift enum, e.g.,

/* assuming this is java.time.Month */
extension Month {
  public enum EnumConstant {
    case JANUARY
    case FEBRUARY
    // ...
  }

  public init(_ constant: EnumConstant, environment: JNIEnvironment) { ... }

  public var enumConstant: EnumConstant { get }
}

Then we could add matching operators for switch and such as needed. Perhaps even factor some of these operations out into a protocol to which Java enum classes would be made to conform. We would no longer require the virtual machine instance nor the try!.
What do you think? I'm open to other ideas, too!

I like it. I think we would need EnumConstant to be computed since we might get the value from Java, which would not have a way of creating the synthesized enum in Swift (I don't think). I'll prototype this setup which gives us the benefit of Swift enums without needing to use the environment and force unwrap everywhere

New update based on the proposal, my one thought would be to update the MonthCases to be camel case, but open to suggestions:

import JavaKit
import JavaRuntime

@JavaClass("java.time.Month")
public struct Month {
  public enum MonthCases: Equatable {
    case JANUARY
    case FEBRUARY
    case MARCH
    case APRIL
    case MAY
    case JUNE
    case JULY
    case AUGUST
    case SEPTEMBER
    case OCTOBER
    case NOVEMBER
    case DECEMBER
  }

  public var enumValue: MonthCases? {
    if self.equals(self.javaClass.JANUARY?.as(JavaObject.self)) {
      return MonthCases.JANUARY
    } else if self.equals(self.javaClass.FEBRUARY?.as(JavaObject.self)) {
      return MonthCases.FEBRUARY
    } else if self.equals(self.javaClass.MARCH?.as(JavaObject.self)) {
      return MonthCases.MARCH
    } else if self.equals(self.javaClass.APRIL?.as(JavaObject.self)) {
      return MonthCases.APRIL
    } else if self.equals(self.javaClass.MAY?.as(JavaObject.self)) {
      return MonthCases.MAY
    } else if self.equals(self.javaClass.JUNE?.as(JavaObject.self)) {
      return MonthCases.JUNE
    } else if self.equals(self.javaClass.JULY?.as(JavaObject.self)) {
      return MonthCases.JULY
    } else if self.equals(self.javaClass.AUGUST?.as(JavaObject.self)) {
      return MonthCases.AUGUST
    } else if self.equals(self.javaClass.SEPTEMBER?.as(JavaObject.self)) {
      return MonthCases.SEPTEMBER
    } else if self.equals(self.javaClass.OCTOBER?.as(JavaObject.self)) {
      return MonthCases.OCTOBER
    } else if self.equals(self.javaClass.NOVEMBER?.as(JavaObject.self)) {
      return MonthCases.NOVEMBER
    } else if self.equals(self.javaClass.DECEMBER?.as(JavaObject.self)) {
      return MonthCases.DECEMBER
    } else {
      return nil
    }
  }

  ... other things ...
}

@DougGregor
Copy link
Member

@jrosen081 I definitely like this direction better. Should we also add an initializer that takes a MonthCases value and a JNI environment and gets the appropriate instance from the self.javaClass? That way we can go in both directions.

I'm mixed on the idea of converting UPPER_SNAKE_CASE to lowerCamelCase. It will feel a bit nicer in Swift, but might make it harder to find the corresponding Java documentation.

@jrosen081
Copy link
Contributor Author

@jrosen081 I definitely like this direction better. Should we also add an initializer that takes a MonthCases value and a JNI environment and gets the appropriate instance from the self.javaClass? That way we can go in both directions.

I'm mixed on the idea of converting UPPER_SNAKE_CASE to lowerCamelCase. It will feel a bit nicer in Swift, but might make it harder to find the corresponding Java documentation.

New initializer:

  public init?(_ enumValue: MonthCases, environment: JNIEnvironment) throws {
    let classObj = try JavaClass<Self>(in: environment)
    switch enumValue {
      case .JANUARY:
        if let JANUARY = classObj.JANUARY {
          self = JANUARY
        } else {
          return nil
        }
      case .FEBRUARY:
        if let FEBRUARY = classObj.FEBRUARY {
          self = FEBRUARY
        } else {
          return nil
        }
      case .MARCH:
        if let MARCH = classObj.MARCH {
          self = MARCH
        } else {
          return nil
        }
      case .APRIL:
        if let APRIL = classObj.APRIL {
          self = APRIL
        } else {
          return nil
        }
      case .MAY:
        if let MAY = classObj.MAY {
          self = MAY
        } else {
          return nil
        }
      case .JUNE:
        if let JUNE = classObj.JUNE {
          self = JUNE
        } else {
          return nil
        }
      case .JULY:
        if let JULY = classObj.JULY {
          self = JULY
        } else {
          return nil
        }
      case .AUGUST:
        if let AUGUST = classObj.AUGUST {
          self = AUGUST
        } else {
          return nil
        }
      case .SEPTEMBER:
        if let SEPTEMBER = classObj.SEPTEMBER {
          self = SEPTEMBER
        } else {
          return nil
        }
      case .OCTOBER:
        if let OCTOBER = classObj.OCTOBER {
          self = OCTOBER
        } else {
          return nil
        }
      case .NOVEMBER:
        if let NOVEMBER = classObj.NOVEMBER {
          self = NOVEMBER
        } else {
          return nil
        }
      case .DECEMBER:
        if let DECEMBER = classObj.DECEMBER {
          self = DECEMBER
        } else {
          return nil
        }
    }
  }

It is not ideal imo to be both throws and failable, but it's probably best since there are really 2 types of errors here (the JNIEnvironment isn't set up right and the mapped values are optional)

@DougGregor
Copy link
Member

@jrosen081 I definitely like this direction better. Should we also add an initializer that takes a MonthCases value and a JNI environment and gets the appropriate instance from the self.javaClass? That way we can go in both directions.
I'm mixed on the idea of converting UPPER_SNAKE_CASE to lowerCamelCase. It will feel a bit nicer in Swift, but might make it harder to find the corresponding Java documentation.

New initializer:

  public init?(_ enumValue: MonthCases, environment: JNIEnvironment) throws {
    let classObj = try JavaClass<Self>(in: environment)
    switch enumValue {
      case .JANUARY:
        if let JANUARY = classObj.JANUARY {
          self = JANUARY
        } else {
          return nil
        }
      case .FEBRUARY:
        if let FEBRUARY = classObj.FEBRUARY {
          self = FEBRUARY
        } else {
          return nil
        }
      case .MARCH:
        if let MARCH = classObj.MARCH {
          self = MARCH
        } else {
          return nil
        }
      case .APRIL:
        if let APRIL = classObj.APRIL {
          self = APRIL
        } else {
          return nil
        }
      case .MAY:
        if let MAY = classObj.MAY {
          self = MAY
        } else {
          return nil
        }
      case .JUNE:
        if let JUNE = classObj.JUNE {
          self = JUNE
        } else {
          return nil
        }
      case .JULY:
        if let JULY = classObj.JULY {
          self = JULY
        } else {
          return nil
        }
      case .AUGUST:
        if let AUGUST = classObj.AUGUST {
          self = AUGUST
        } else {
          return nil
        }
      case .SEPTEMBER:
        if let SEPTEMBER = classObj.SEPTEMBER {
          self = SEPTEMBER
        } else {
          return nil
        }
      case .OCTOBER:
        if let OCTOBER = classObj.OCTOBER {
          self = OCTOBER
        } else {
          return nil
        }
      case .NOVEMBER:
        if let NOVEMBER = classObj.NOVEMBER {
          self = NOVEMBER
        } else {
          return nil
        }
      case .DECEMBER:
        if let DECEMBER = classObj.DECEMBER {
          self = DECEMBER
        } else {
          return nil
        }
    }
  }

It is not ideal imo to be both throws and failable, but it's probably best since there are really 2 types of errors here (the JNIEnvironment isn't set up right and the mapped values are optional)

Perhaps it should be neither? The try is only there for cases where the JVM literally doesn't have the right classes loaded, which feels like it should be fatal in Swift, so I'd be okay with a try! there. The switch only fails if there is effectively an ABI "break" on the Java side, where a constant that existed at the time that Java2Swift runs but not when the program runs. Is that something we need to deal with?

Irrespective of our decisions on the above, I'm really liking this direction! Once you're happy with it, I recommend regenerating the java2swift sources with make javakit-generate.

@jrosen081
Copy link
Contributor Author

@jrosen081 I definitely like this direction better. Should we also add an initializer that takes a MonthCases value and a JNI environment and gets the appropriate instance from the self.javaClass? That way we can go in both directions.
I'm mixed on the idea of converting UPPER_SNAKE_CASE to lowerCamelCase. It will feel a bit nicer in Swift, but might make it harder to find the corresponding Java documentation.

New initializer:

  public init?(_ enumValue: MonthCases, environment: JNIEnvironment) throws {
    let classObj = try JavaClass<Self>(in: environment)
    switch enumValue {
      case .JANUARY:
        if let JANUARY = classObj.JANUARY {
          self = JANUARY
        } else {
          return nil
        }
      case .FEBRUARY:
        if let FEBRUARY = classObj.FEBRUARY {
          self = FEBRUARY
        } else {
          return nil
        }
      case .MARCH:
        if let MARCH = classObj.MARCH {
          self = MARCH
        } else {
          return nil
        }
      case .APRIL:
        if let APRIL = classObj.APRIL {
          self = APRIL
        } else {
          return nil
        }
      case .MAY:
        if let MAY = classObj.MAY {
          self = MAY
        } else {
          return nil
        }
      case .JUNE:
        if let JUNE = classObj.JUNE {
          self = JUNE
        } else {
          return nil
        }
      case .JULY:
        if let JULY = classObj.JULY {
          self = JULY
        } else {
          return nil
        }
      case .AUGUST:
        if let AUGUST = classObj.AUGUST {
          self = AUGUST
        } else {
          return nil
        }
      case .SEPTEMBER:
        if let SEPTEMBER = classObj.SEPTEMBER {
          self = SEPTEMBER
        } else {
          return nil
        }
      case .OCTOBER:
        if let OCTOBER = classObj.OCTOBER {
          self = OCTOBER
        } else {
          return nil
        }
      case .NOVEMBER:
        if let NOVEMBER = classObj.NOVEMBER {
          self = NOVEMBER
        } else {
          return nil
        }
      case .DECEMBER:
        if let DECEMBER = classObj.DECEMBER {
          self = DECEMBER
        } else {
          return nil
        }
    }
  }

It is not ideal imo to be both throws and failable, but it's probably best since there are really 2 types of errors here (the JNIEnvironment isn't set up right and the mapped values are optional)

Perhaps it should be neither? The try is only there for cases where the JVM literally doesn't have the right classes loaded, which feels like it should be fatal in Swift, so I'd be okay with a try! there. The switch only fails if there is effectively an ABI "break" on the Java side, where a constant that existed at the time that Java2Swift runs but not when the program runs. Is that something we need to deal with?

Didn't think of it like that. Went with a try! on the environment (if it fails there are bigger issues). For the switch failing, I added a fatalError to explain what is going wrong and how to likely fix it.

Irrespective of our decisions on the above, I'm really liking this direction! Once you're happy with it, I recommend regenerating the java2swift sources with make javakit-generate.

I ran it, but nothing ended up changing (I don't think we are converting any enums right now)

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

I left one small comment. This looks great!

@@ -77,6 +77,7 @@ extension JavaTranslator {
package static let defaultTranslatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = [
"java.lang.Class": ("JavaClass", "JavaKit", true),
"java.lang.String": ("String", "JavaKit", false),
"java.lang.Object": ("JavaObject", "JavaKit", true)
Copy link
Member

Choose a reason for hiding this comment

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

I believe this change shouldn't be necessary. java.lang.Object is already listed in Java2Swift.config for the JavaKit target, so the only way this could be needed is if something is missing a JavaKit dependency.

@DougGregor
Copy link
Member

Irrespective of our decisions on the above, I'm really liking this direction! Once you're happy with it, I recommend regenerating the java2swift sources with make javakit-generate.

I ran it, but nothing ended up changing (I don't think we are converting any enums right now)

Okay! I'm sure we'll find some more enums as we bring more of the Java base types into the JavaKit modules.

@ktoso
Copy link
Collaborator

ktoso commented Oct 20, 2024

LGTM! Looking good, thanks for the contribution! :-)

@DougGregor DougGregor merged commit b9122bd into swiftlang:main Oct 20, 2024
11 checks passed
@DougGregor
Copy link
Member

@jrosen081 merged, thank you!

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.

Implement Swift wrappers for Java enum types
3 participants