Skip to content
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

Add support for enums with associated data (aka "tagged unions", aka "sum types", aka ...) #381

Merged
merged 1 commit into from
Feb 22, 2021

Conversation

rfk
Copy link
Collaborator

@rfk rfk commented Feb 8, 2021

Our fxa-client component makes use of a Rust enum with associated data to represent the different kinds of event that can happen to your account. The Rust code looks like this:

pub enum AccountEvent {
    IncomingDeviceCommand(Box<IncomingDeviceCommand>),
    ProfileUpdated,
    DeviceConnected {
        device_name: String,
    },
    [...other cases omitted...]
}

In the hand-written bindings, we reflect this to Kotlin as a sealed class and reflect it to Swift as an enum with associated data.

UniFFI doesn't yet support this kind of data structure, which makes it awkward to use for the fxa-client component. So let's see if we can add it!

WebIDL doesn't seem to have a notion of a datatype like this or offer any ready syntax for it (It has union types, but they're like C-style unions and do not have an explicit discriminant). I messed around a bit and came up with a syntax that doesn't seem too bad, and is similar to how we're implemented errors. This PR contains just the docs and test changes that would happen if we chose to implement tagged unions in this manner. I'm looking for early feedback on whether this seems like a good idea.

/cc @mhammond @lougeniaC64 @jhugman @dmose

V4(u8 q1, u8 q2, u8 q3, u8 q4);
V6(string addr);
};
```
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the proposed UDL syntax. I think it looks nice, but it's a pretty serious abuse of WebIDL syntax. It parses, but e.g. the V4 line actually parses as an anonymous special method returning a datatype named V4 rather than as a field or method named V4. I personally have no problem with us re-interpreting that as a tagged union, but it seems gross so YMMV. I haven't been able to come up with anything better though :-/

I had hoped to be able to do something clever with the existing WebIDL enum declaration, along the lines of:

[TaggedUnion]
enum IpAddr {
  [Fields(u8)] "V4",
  [Fields(u8, string)] "V6",
}

But I couldn't come up with something that would parse. In particular, attributes don't seem to be supported on the variants of an enum.

Copy link
Contributor

Choose a reason for hiding this comment

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

I love the syntax you've arrived at.

This and #379 reminds me we never quite resolved the question of writing our own parser. Soon is coming the time to reconsider ADR-0001!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Soon is coming the time to reconsider ADR-0001!

Yes! I have many half-baked thoughts on this. What do you think is a good way to structure that kind of discussion, should we e.g. open an open-ended "discuss" issue where we can kick some ideas around?

Copy link
Member

Choose a reason for hiding this comment

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

+1 to what James said, although I'm slightly more skeptical we will find time to rewrite the parser until that's our final option :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Mark here: I expect we'll only seriously consider doing the work once we've exhausted.

That'd be the wrong time for a discussion, so yes, let's start a doc/issue and iterate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That'd be the wrong time for a discussion, so yes, let's start a doc/issue and iterate.

Sounds good 👍

(FWIW, I was pretty close to declaring "WebIDL has no good syntax for this" before I discovered the anonymous-special-method-returning-a-named-datatype hackery you see here, so I won't be surprised if we run out of runway with WebIDL sooner than we expect)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I filed a "discuss" issue here: #385

"0" to EnumerationAvecDonnees.ZERO(),
"1" to EnumerationAvecDonnees.UN(1),
"2" to EnumerationAvecDonnees.DEUX(2, "deux")
))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(I haven't actually run any of these examples, just trying my best to sketch out what the resulting code would actually look like to use).

Copy link
Contributor

@jhugman jhugman Feb 8, 2021

Choose a reason for hiding this comment

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

In Kotlin, since they're inner classes, they'd be spelled with CamelCase.

They'd be defined as:

sealed class EnumerationAvecDonnees {
    object Zero: EnumerationAvecDonnees() // object for noargs
    data class Un(val value: Int): EnumerationAvecDonnees()
    data class Deux(val value: Int, val nombre: String): EnumerationAvecDonnees()
}

assert(EnumerationAvecDonnees.Zero != EnumerationAvecDonnees.Deux(2, "two"))

(in retrospect, this comment feels like "Well Actually". Sorry)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(in retrospect, this comment feels like "Well Actually". Sorry)

Heh, no worries at all, exactly the sort of early feedback I'm after 👍

@rfk
Copy link
Collaborator Author

rfk commented Feb 8, 2021

I haven't even begun to think about what this would look like for the Gecko-JS bindings.

Can be exposed in the UDL file with:

```idl
[TaggedUnion]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please feel welcome to propose your favourite colour for this bikeshed. I chose "tagged union" because that's the canonical name of the Wikipedia page about this concept.

Copy link
Member

Choose a reason for hiding this comment

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

heh - see above - I mildly prefer "tagged enum" given we are in rust.

@rfk rfk force-pushed the component-interface-deconstruction branch 2 times, most recently from 9fd863c to b2fb6f8 Compare February 9, 2021 01:38
Base automatically changed from component-interface-deconstruction to main February 9, 2021 01:42
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

This is awesome. I skipped over the parser code, but will try and get back to it over the next few days, but thought I'd share my hearty +1 in the meantime.

README.md Outdated
@@ -88,7 +88,7 @@ Things that are implemented so far:

* Primitive numeric types, equivalents to those offered by Rust (`u32`, `f64`, etc).
* Strings (which are always UTF-8, like Rust's `String`).
* C-style enums (just the discriminant, no associated data).
* Enums, including enums associated data (aka "Tagged Unions").
Copy link
Member

Choose a reason for hiding this comment

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

nit: add "with"

@@ -85,6 +86,7 @@ The details of this format are internal only and may change between versions of
| `sequence<T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `T` |
| `record<string, T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `string` followed by a serialized `T` |
| `enum` | Serialized `u32` indicating variant, numbered in declaration order starting from 1 |
| `[TaggedUnion] interface` | Serialized `u32` indicating variant as per `enum`, followed by the serialized value of each field, in declaration order |
Copy link
Member

Choose a reason for hiding this comment

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

I don't think "union" and "enum" are simple synonyms in rust - is TaggedEnum a better name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, that's a good point re: the likelihood the name here will be interpreted somewhere close to Rust's semantics.

Copy link
Contributor

Choose a reason for hiding this comment

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

I spent a good deal of time thinking about this yesterday, and still couldn't remember TaggedUnion. Having said that: what a lovely bikeshed!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm going to try just [Enum] and see how I like it...

Can be exposed in the UDL file with:

```idl
[TaggedUnion]
Copy link
Member

Choose a reason for hiding this comment

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

heh - see above - I mildly prefer "tagged enum" given we are in rust.

V4(u8 q1, u8 q2, u8 q3, u8 q4);
V6(string addr);
};
```
Copy link
Member

Choose a reason for hiding this comment

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

+1 to what James said, although I'm slightly more skeptical we will find time to rewrite the parser until that's our final option :)

};
```

Only enums with named fields are supported by this syntax.
Copy link
Member

Choose a reason for hiding this comment

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

(to reinforce the naming thing - the docs keep calling them "enums" :)

@@ -2,7 +2,7 @@ namespace rondpoint {
Dictionnaire copie_dictionnaire(Dictionnaire d);
Enumeration copie_enumeration(Enumeration e);
sequence<Enumeration> copie_enumerations(sequence<Enumeration> e);
record<DOMString, Enumeration> copie_carte(record<DOMString, Enumeration> c);
record<DOMString, EnumerationAvecDonnees> copie_carte(record<DOMString, EnumerationAvecDonnees> c);
Copy link
Member

Choose a reason for hiding this comment

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

A big +1 for the french :)

(On a completely less serious note, I wonder if the French in rondpoint is a bigger risk than the parser in the shorter term ;)

Copy link
Collaborator Author

@rfk rfk Feb 9, 2021

Choose a reason for hiding this comment

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

On the other hand, trying to switch this away from French would be like going through the Sync codebase and removing all the puns...I don't think I'm ready for that just yet 😢 😅

Copy link
Member

Choose a reason for hiding this comment

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

How about writing newly added code in English, and changing bits of codes that we touch incrementally, as it makes sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for @dmose 's new code in English. I'd add the innovation of breaking up rondpoint into multiple examples: e.g. primitives, collections, enums, callbacks, etc.

@rfk rfk force-pushed the tagged-unions branch 2 times, most recently from 45554da to 36e07d7 Compare February 9, 2021 09:57
@rfk
Copy link
Collaborator Author

rfk commented Feb 9, 2021

A quick update here - there are plenty of rough bits remaining, but the modified Kotlin test seems to be running and working as intended!

// Kotlin's `enum class` constuct doesn't support variants with associated data,
// but is a little nicer for consumers than its `sealed class` enum pattern.
// So, we switch here, using `enum class` for enums with no associated data
// and `sealed class` for the general case.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jhugman here's a design question I'd appreciate your insight on. Kotlin's enum class declaration doesn't support associated data in the way we want to use it, so in the general case we have to use the sealed class approach. However, when there isn't any associated data, Kotlin's enum seems to be a little nicer to work with. Here I've opted to switch between them - enums without data will be an enum class just as they were under the previous implementation, while newly-supported enums with data will be a sealed class.

This has the nice property of being backwards-compatible for existing consumers of existing UDL files. But I'm a little concerned about potential developer confusion in future.

For example, suppose someone currently has a plain-old-enum with no data in their UDL file, and their Kotlin consumers are consuming it through an enum class. The component API evolves and adds a new case to the enum, this time with some associated data. For Kotlin consumers this addition will change the entire type of the thing they're dealing with, including changing the capitalization of existing items as it switches from being an enum class to a sealed class.

Maybe this is OK, since consumers will need to update their code to handle the new variant anyway. Or maybe it's leaking implementation details in a way that we shouldn't need to do.

Thoughts?

Copy link
Contributor

@jhugman jhugman Feb 9, 2021

Choose a reason for hiding this comment

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

It's a great question.

From our (nimbus + uniffi) point of view, I don't think it matters: we have a tiny amount of code using UDL enums, so changing them will have some coordination cost when changing them to a new format.

For android devs: adding another enum variant is going to be a breaking change.

There is some JVM cost to using sealed classes over Java style (around class initialization, maximum method counts etc). I don't think we're at the stage we need to worry about that.

While thinking about this, it did strike me that Kotlin has had to make a similar choice: Java enums existed after Java5. Kotlin came up with a better way, yet still supported old school Java enums. I should imagine their backwards compatibility problem was more persuasive/pressing than ours.

Having thought about it, I have a stong-preference-weakly-held for keeping the implementing UDL's enum as enum class in Kotlin.

Copy link
Contributor

Choose a reason for hiding this comment

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

Other things this question made me think about:

Until we have a idiomatically consumable version of tagged unions in JS, we're going to be discouraging use of this feature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Until we have a idiomatically consumable version of tagged unions in JS, we're going to be discouraging use of this feature.

Yeah. Python has a similar problem, which is one of the reasons I like having the python backend here, to think about how things will look in a language with basically the opposite of a Rust-style strong algebraic type system.

@dmose
Copy link
Member

dmose commented Feb 10, 2021

As you mentioned above, the GeckoJS stuff is going to be the higher risk. The thing that jumps out at me is wondering whether the stuff you've done for UDL is even going to be expressable in a way that the Firefox WebIDL parser will understand...

@rfk rfk force-pushed the tagged-unions branch 2 times, most recently from ab5ed10 to 73cfb08 Compare February 15, 2021 02:51
@rfk rfk changed the title Add support for tagged unions (aka "enums with associated data", aka "sum types", aka ...) Add support for enums with associated data (aka "tagged unions", aka "sum types", aka ...) Feb 15, 2021
@rfk
Copy link
Collaborator Author

rfk commented Feb 15, 2021

I've got this working in a way that I'm pretty happy with for the Kotlin, Swift and Python backends. I haven't tackled the gecko_js backend yet, and similar to my comments here, I think we should have a little sync-up about how we'll approach that backend strategically in the short-term. I'm happy to do the work! I just don't want to get stuck down a rabbit-hole if it's not strategically important right now.

(Unlike #384, I don't really have a good idea for how to expose these through C++ and into special Gecko JS. We could try to do something similar to what the python backend is currently doing here, basically creating a separate class for each variant and packaging them up into a convenient API...but I can easily imagine that not playing nicely with all the C++ codegen and what-not).

@rfk
Copy link
Collaborator Author

rfk commented Feb 16, 2021

A note to self from trying this out in the fxa-client component: we'll need to be careful with name conflicts at different scopes.

The fxa-client declared a set of enums like this:

[Enum]
interface AccountEvent {
  IncomingDeviceCommand(IncomingDeviceCommand command );
  ProfileUpdated();
  AccountAuthStateChanged();
  AccountDestroyed();
  DeviceConnected(string device_name );
  DeviceDisconnected(string device_id, boolean is_local_device );
};

[Enum]
interface IncomingDeviceCommand {
  TabReceived(Device? sender, SendTabPayload payload );
};

Note that the name IncomingDeviceCommand is both a top-level enum, and one of the variants on the AccountEvent enum. When I generated the Kotlin code for this, we wound up with:

sealed class IncomingDeviceCommand {
    class TabReceived(
        val sender: Device?,
        val payload: SendTabPayload
    ) : IncomingDeviceCommand() { }

    companion object {
        internal fun read(buf: ByteBuffer): IncomingDeviceCommand {
            // details omitted
        }
    }
}

sealed class AccountEvent {
    class IncomingDeviceCommand(
        val command: IncomingDeviceCommand
    ) : AccountEvent() { ... }

    // Other cases omitted

    companion object {
        internal fun read(buf: ByteBuffer): AccountEvent {
            return when (buf.getInt()) {
                1 -> AccountEvent.IncomingDeviceCommand(
                    //
                    //  +---- this line gives a compile error
                    //  |
                    //  v
                    IncomingDeviceCommand.read(buf)
                )
                // other cases omitted
                else -> throw RuntimeException("invalid enum value, something is very wrong!!")
            }
        }
    }
}

This doesn't compile, because Kotlin thinks that IncomingDeviceCommand.read(buf) is referring to the inner IncomingDeviceCommand class defined on the AccountEvent enum, rather than the top-level IncomingDeviceCommand class.

I've worked around this for now by avoiding the name conflict, but we should figure out a better approach before landing this PR. Maybe there's a way to do the codegen so that the names are not ambiguous, or maybe we need to just check for duplicate names and error in that case.

At the very least, we need a testcase for this.

@rfk rfk force-pushed the tagged-unions branch 2 times, most recently from 5de2a4d to fe06656 Compare February 18, 2021 01:40
@rfk
Copy link
Collaborator Author

rfk commented Feb 18, 2021

Alrighty, I've rebased this atop latest main, and added an early bail out if we see an enum variant name that conflicts with a top-level type name.

@rfk rfk requested review from jhugman and mhammond February 18, 2021 01:41
struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> {
[[nodiscard]] static bool Lift(const uint32_t& aLowered, {{ e.name()|class_name_cpp(context) }}& aLifted) {
switch (aLowered) {
struct Serializable<{{ e.name()|class_name_cpp(context) }}> {
Copy link
Collaborator Author

@rfk rfk Feb 18, 2021

Choose a reason for hiding this comment

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

This PR changes the way enums are passed over the FFI, even enums without any associated data. I've done my best to update this and will see if I can try it out in mozilla-central following the instructions in this doc but I don't have high confidence in my skills in this area.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was able to integrate this into m-c and have it compile, then fail on a linker error. So I guess that's a good sign.

@rfk rfk force-pushed the tagged-unions branch 2 times, most recently from 5ec3b08 to f46d83c Compare February 18, 2021 04:41
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

This looks great with the usual attention to detail we've come to expect from your PRs :) r+ from me, but I'll let James give formal approval once he has cast his eye again over the generated swift/kotlin


# Now, a little trick - we make each nested variant class by a subclass of the main
# enum subclass, so that method calls and instance checks etc will work intuitively.
# We might be able to do this a little more nearly with a metaclass, but this'll do.
Copy link
Member

Choose a reason for hiding this comment

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

nit: neatly?

@@ -97,7 +97,7 @@ def writeF64(self, v):
{% when Type::Boolean -%}

def writeBool(self, v):
self._pack_into(1, ">b", 0 if v else 1)
self._pack_into(1, ">b", 1 if v else 0)
Copy link
Member

Choose a reason for hiding this comment

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

this is an unrelated bugfix, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, yes, yes it is - but one that I only happened to discover in the process of getting all this working.

Copy link
Contributor

@jhugman jhugman left a comment

Choose a reason for hiding this comment

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

I'm pretty happy with how this is coming along; I have a few questions, but this is looking really close.

@@ -2,7 +2,7 @@ namespace rondpoint {
Dictionnaire copie_dictionnaire(Dictionnaire d);
Enumeration copie_enumeration(Enumeration e);
sequence<Enumeration> copie_enumerations(sequence<Enumeration> e);
record<DOMString, Enumeration> copie_carte(record<DOMString, Enumeration> c);
record<DOMString, EnumerationAvecDonnees> copie_carte(record<DOMString, EnumerationAvecDonnees> c);
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for @dmose 's new code in English. I'd add the innovation of breaking up rondpoint into multiple examples: e.g. primitives, collections, enums, callbacks, etc.


{% else %}

enum class {{ e.name()|class_name_kt }} {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm glad you went for this in the end.

Comment on lines 4 to 6
{% for field in variant.fields() %}
{{ field.name()|var_name_swift }}: {{ field.type_()|type_swift }}{%- if loop.last -%}{%- else -%},{%- endif -%}
{% endfor %}
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks very like an already existing macro: swift:arg_list_decl, IIRC.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's copy-pasted from the logic for declaring record fields. I'm going to extract them into a swift::field_list_decl macro for convenience.

@@ -1,30 +1,40 @@
public enum {{ e.name()|class_name_swift }}: ViaFfi {
public enum {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi, Equatable {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should address it yet, but at some point (hopefully) someone is going to file a bug about a missing indirect keyword here so we can support recursive enums. What do you think: should we file an issue about it, and put the link in as a comment here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, I filed #396 for future discussions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, OK.

My thought was that a developer gets an error swiftc pointing to the generated code, goes and looks at the generate code, finds the comment with the link to the issue. WDYT?

This review cycle has been long, so I wouldn't be against leaving it to another issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense. I think in practice they will probably get an error before they even try to generate swift code, because the Rust code will need to use some Box indirection and the generated scaffolding won't support it. But I can add a link to the issue in a few key places in the hope that any such users will find it.

// Can't be both `[Threadsafe]` and an `[Enum]`.
if attrs.len() > 1 {
bail!("conflicting attributes on interface definition");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ha. Nice.

}

pub fn has_associated_data(&self) -> bool {
self.variants.iter().any(Variant::has_fields)
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC: I think this should be more about how it was defined in the UDL, rather than the content of the variants, since it may produce some surprising changes in the bindings:

[Enum]
interface Foo {
    Bar
}

changing to

[Enum]
interface Foo {
    Bar(string baz)
}

will change from Foo.BAR to Foo.Bar(baz: String)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I think you're right. I'm not super happy about it because it feels like an implementation detail of the (what we hope to be temporary) IDL leaking all the way through to the generated bindings, but that's probably less bad than having a confusing user experience.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm updating this to mark enums that come from enum foo as "flat" and enums that come from [Enum] interface as "not flat", and will use that in codegen to switch between the two implementations.


sealed class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() -%}
class {{ variant.name()|class_name_kt }}({% if variant.has_fields() %}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: variants without fields should be objects.

Suggested change
class {{ variant.name()|class_name_kt }}({% if variant.has_fields() %}
{% if !variant.has_fields() -%}
object {{ variant.name()|class_name_kt }} : {{ e.name()|class_name_kt }}
{% else -%}
class {{ variant.name()|class_name_kt }}(

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FWIW, I mildly dislike the asymmetry this produces between EnumerationAvecDonnees.Zero without parens and EnumerationAvecDonnees.Un(1u) with parens, but am happy to defer to your judgement here.

{% for field in variant.fields() -%}
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %}
{% endfor -%}
{%- endif %}) : {{ e.name()|class_name_kt }}() {
Copy link
Contributor

Choose a reason for hiding this comment

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

In the same vein:

Suggested change
{%- endif %}) : {{ e.name()|class_name_kt }}() {
: {{ e.name()|class_name_kt }}() { {%- endif %})

Comment on lines 24 to 34
if (other is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}) {
{% if variant.has_fields() -%}
{% for field in variant.fields() -%}
{{ field.name()|var_name_kt }} == other.{{ field.name()|var_name_kt }}{% if loop.last %}{% else %} && {% endif -%}
{% endfor -%}
{% else -%}
true
{%- endif %}
} else {
false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I see what's happening here, there are two ways I can suggest to improve it:

Suggested change
if (other is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}) {
{% if variant.has_fields() -%}
{% for field in variant.fields() -%}
{{ field.name()|var_name_kt }} == other.{{ field.name()|var_name_kt }}{% if loop.last %}{% else %} && {% endif -%}
{% endfor -%}
{% else -%}
true
{%- endif %}
} else {
false
}
(other is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }})
{% for field in variant.fields() -%}
&& {{ field.name()|var_name_kt }} == other.{{ field.name()|var_name_kt }}
{% endfor -%}

The second is the more idiomatic way of doing things in Kotlin: by doing it in a single equals method and the outer Enum level.

sealed class Enum {
    object Zero : Enum()
    class One(val v: Int) : Enum()
    class Two(val a: Int, val b: Int) : Enum()

    fun equals(other: Any) =
        when {
            this is Zero && other is Zero -> true
            this is One && other is One -> this.v == other.v
            this is Two && other is Two -> this.a == other.a && this.b == other.b
            else -> false
        }
}

I was trying to do something with destructuring of Pair, so we could when (this to other) but gave up.

The galaxy brain way is: to use data classes, which generates the equals, hashCode, toString() and copy for us.

sealed class Enum {
    object Zero : Enum()
    data class One(val v: Int) : Enum()
    data class Two(val a: Int, val b: Int) : Enum()
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🤯

The galaxy-brain approach seems to work a treat, and I'll add a couple more testcases around equality to ensure that. Very nice!

}

internal open fun write(buf: RustBufferBuilder) {
throw RuntimeException("enum variant should have overridden `write` method, something is very wrong!!")
Copy link
Contributor

Choose a reason for hiding this comment

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

On reflection, I think I'd rather put the write method and friends in the top level, and use pattern matching to work out what to do.

(FWIW I don't think there's any perf difference, in fact: given it's thee idiomatic way of doing tagged-unions, I expect there's some particular optimizations we could end up losing out on by adding methods to individual variant classes.)

@rfk
Copy link
Collaborator Author

rfk commented Feb 19, 2021

Updated in response to @jhugman latest feedback.


val var1: EnumerationAvecDonnees = EnumerationAvecDonnees.Zero
val var2: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(1u)
val var3: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(2u)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The typed variables here are to outsmart the Kotlin compiler, which will give a compile-time error if you try to directly compare something of type EnumerationAvecDonnees.Zero and something of type EnumerationAvecDonnees.Un.

@rfk
Copy link
Collaborator Author

rfk commented Feb 19, 2021

Hrm, the rondpoint crate is failing to build in CI here, with what appear to be out-of-memory errors:

Caused by:
  process didn't exit successfully: `rustc --crate-name uniffi_rondpoint --edition=2018 examples/rondpoint/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type cdylib --crate-type lib --emit=dep-info,link -C debuginfo=2 -C metadata=053719643b56441c --out-dir /home/circleci/project/target/debug/deps -C incremental=/home/circleci/project/target/debug/incremental -L dependency=/home/circleci/project/target/debug/deps --extern uniffi=/home/circleci/project/target/debug/deps/libuniffi-622b493c8a6725e6.rlib --extern uniffi_macros=/home/circleci/project/target/debug/deps/libuniffi_macros-b7aff2897c4d9de6.so -D warnings` (signal: 9, SIGKILL: kill)
warning: build failed, waiting for other jobs to finish...
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-L" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/home/circleci/project/target/debug/deps/arithmetical.128vv0nl8ohl5duk.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.17qde80626cmwono.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.190shw0nb0jwrvnq.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.1efkqkyw2vfvq0cq.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.1h549mnvlhu6kx62.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.1trgsh1jgtu2dzg4.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.201cn5r1kdf42vwp.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.23xtc1yr9w6voutq.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.249yaojpywzbmc58.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.26tnl9p6edsmo1vn.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.2b8zs3xp8yccjv9n.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.2dfznzhzko3hfucn.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.2o3y1jvfz588xr4j.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.2whez0kgj4a6yh6v.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.31ziggzrgykvtvvh.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.3fo6cwgekbm8eipp.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.3smn1wl22kf53084.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.42sg7z6pu41rx0oj.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.4blho4emn5w3yt1w.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.4ssytj083kstompf.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.544wkcpgqb1z1i1a.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.56t3ybnmrjh0xjzu.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.5fgy3snlmii7kl8g.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.dsblm0i1ak2u4v8.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.nqushftut1gb8rg.rcgu.o" "/home/circleci/project/target/debug/deps/arithmetical.nwud4vssfr9v9r5.rcgu.o" "-o" "/home/circleci/project/target/debug/deps/libarithmetical.so" "-Wl,--version-script=/tmp/rustcImMpM0/list" "/home/circleci/project/target/debug/deps/arithmetical.hkx95sgdce0u0cv.rcgu.o" "-Wl,--gc-sections" "-Wl,-zrelro" "-Wl,-znow" "-nodefaultlibs" "-L" "/home/circleci/project/target/debug/deps" "-L" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,-Bstatic" "/home/circleci/project/target/debug/deps/libuniffi-622b493c8a6725e6.rlib" "/home/circleci/project/target/debug/deps/libuniffi_bindgen-16e505356ba326f5.rlib" "/home/circleci/project/target/debug/deps/libweedle-aef167c1aa3dbb32.rlib" "/home/circleci/project/target/debug/deps/libnom-0fee0736e68b1069.rlib" "/home/circleci/project/target/debug/deps/libheck-e9f9f2619703ceaa.rlib" "/home/circleci/project/target/debug/deps/libunicode_segmentation-c8f494fc70b1117f.rlib" "/home/circleci/project/target/debug/deps/libaskama-537ffd130493464b.rlib" "/home/circleci/project/target/debug/deps/libaskama_shared-9451c3a778333d62.rlib" "/home/circleci/project/target/debug/deps/libtoml-31756e16259888ea.rlib" "/home/circleci/project/target/debug/deps/libsyn-1cb4f73bae3a226d.rlib" "/home/circleci/project/target/debug/deps/libnom-9b9d4b929f8fedf4.rlib" "/home/circleci/project/target/debug/deps/libmemchr-b63c81c95b3ea372.rlib" "/home/circleci/project/target/debug/deps/liblexical_core-5908b33ac15872fb.rlib" "/home/circleci/project/target/debug/deps/libarrayvec-fc27a13ca7389030.rlib" "/home/circleci/project/target/debug/deps/libbitflags-599c3a9be4aadd44.rlib" "/home/circleci/project/target/debug/deps/libbitvec-c3fcb913cff8ff19.rlib" "/home/circleci/project/target/debug/deps/libfunty-19a720bfa18dce89.rlib" "/home/circleci/project/target/debug/deps/libwyz-4577ea1179b041c6.rlib" "/home/circleci/project/target/debug/deps/libtap-fd238052b670c4b5.rlib" "/home/circleci/project/target/debug/deps/libradium-0a3a3c08ef28929c.rlib" "/home/circleci/project/target/debug/deps/libquote-7e1852a0cea0cdb2.rlib" "/home/circleci/project/target/debug/deps/libproc_macro2-815d44389fbca503.rlib" "/home/circleci/project/target/debug/deps/libunicode_xid-5bbc8b1bac57b67c.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libproc_macro-9fd29953164d86f0.rlib" "/home/circleci/project/target/debug/deps/libaskama_escape-ed1e0cc3b58d2466.rlib" "/home/circleci/project/target/debug/deps/libstatic_assertions-7b24323df9107fb0.rlib" "/home/circleci/project/target/debug/deps/libcargo_metadata-e1494bb5b6f651dd.rlib" "/home/circleci/project/target/debug/deps/libserde_json-e2226d212e2ed73f.rlib" "/home/circleci/project/target/debug/deps/libryu-65130c6d4b3381b5.rlib" "/home/circleci/project/target/debug/deps/libitoa-eca5c176e5c68ac7.rlib" "/home/circleci/project/target/debug/deps/libsemver-7b8295dac8c0b8df.rlib" "/home/circleci/project/target/debug/deps/libserde-81a5e1ceab613554.rlib" "/home/circleci/project/target/debug/deps/libsemver_parser-906f2c60081c06fb.rlib" "/home/circleci/project/target/debug/deps/libffi_support-0e7f42b523b4e0fd.rlib" "/home/circleci/project/target/debug/deps/liblazy_static-5390404b369a64d3.rlib" "/home/circleci/project/target/debug/deps/liblog-f8ad7d725fc2e075.rlib" "/home/circleci/project/target/debug/deps/libcfg_if-bba59873400967df.rlib" "/home/circleci/project/target/debug/deps/libbytes-040a8c7b9588319c.rlib" "/home/circleci/project/target/debug/deps/libanyhow-34a3b06fd061f9d1.rlib" "/home/circleci/project/target/debug/deps/libthiserror-bae65f7bcdf0be60.rlib" "-Wl,--start-group" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-7c5e456310a1373c.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-b981d9b2a408308f.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libhashbrown-43d0ea1b5ae34d0d.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_alloc-09e7f22e773899cd.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace-aa74f166651adf6e.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace_sys-22c386707b639611.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_demangle-db04c9c5cd3bcf45.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunwind-bb27492f721492e8.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcfg_if-6b95245dbf686e20.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liblibc-287409d75db2ecd3.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liballoc-a93f70ee2006b6e3.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_core-566cdfbcc94b4360.rlib" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-7bb8dddc7ce34e92.rlib" "-Wl,--end-group" "/usr/local/rustup/toolchains/1.43.0-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-b117658e17259aa6.rlib" "-Wl,-Bdynamic" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil" "-shared"
  = note: /usr/bin/ld: final link failed: Cannot allocate memory
          collect2: error: ld returned 1 exit status

I'm kind of hoping it's a transient infrastructure issue, because I haven't made any changes to the Rust code there since the previous iteration of this PR, which compiled successfully.

Copy link
Contributor

@jhugman jhugman left a comment

Choose a reason for hiding this comment

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

This is great.

I'm not qualified to comment about Python, or gecko-js, but Swift and Kotlin get my thumbs up.

}

{% endif %}
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole file: lovely.

@@ -1,30 +1,40 @@
public enum {{ e.name()|class_name_swift }}: ViaFfi {
public enum {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi, Equatable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, OK.

My thought was that a developer gets an error swiftc pointing to the generated code, goes and looks at the generate code, finds the comment with the link to the issue. WDYT?

This review cycle has been long, so I wouldn't be against leaving it to another issue.

pub(super) variants: Vec<String>,
pub(super) variants: Vec<Variant>,
// "Flat" enums do not have, and will never have, variants with associated data.
pub(super) flat: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +467 to +476
for e in self.enums.iter() {
for variant in e.variants.iter() {
if self.types.get_type_definition(variant.name()).is_some() {
bail!(
"Enum variant names must not shadow type names: \"{}\"",
variant.name()
)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok.

This commit adds support for enums variants having named data fields,
in a style similar to records. It also attempts to expose both "plain"
enums and the new data-bearing enums to target foreign languages in
an idiomatic way.

Unfortunately for us, WebIDL doesn't have native syntax for this kind
of data. Fortunately for us, we can fake it by using anonymous special
interface methods via a syntax like:

```
[Enum]
interface EnumWithData {
  VariantName(type1 name1, type2 name2, ...);
}
```
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.

4 participants