-
Notifications
You must be signed in to change notification settings - Fork 234
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
Conversation
V4(u8 q1, u8 q2, u8 q3, u8 q4); | ||
V6(string addr); | ||
}; | ||
``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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") | ||
)) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 👍
I haven't even begun to think about what this would look like for the Gecko-JS bindings. |
docs/manual/src/udl/enumerations.md
Outdated
Can be exposed in the UDL file with: | ||
|
||
```idl | ||
[TaggedUnion] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
9fd863c
to
b2fb6f8
Compare
There was a problem hiding this 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"). |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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...
docs/manual/src/udl/enumerations.md
Outdated
Can be exposed in the UDL file with: | ||
|
||
```idl | ||
[TaggedUnion] |
There was a problem hiding this comment.
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); | ||
}; | ||
``` |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
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 😢 😅
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
45554da
to
36e07d7
Compare
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- Typescript can do Tagged Unions, and whatever Javascript can support natively.
- Javascript has barely has the simple enums.
Until we have a idiomatically consumable version of tagged unions in JS, we're going to be discouraging use of this feature.
There was a problem hiding this comment.
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.
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... |
ab5ed10
to
73cfb08
Compare
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). |
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:
Note that the name
This doesn't compile, because Kotlin thinks that 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. |
5de2a4d
to
fe06656
Compare
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. |
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) }}> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
5ec3b08
to
f46d83c
Compare
There was a problem hiding this 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. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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); |
There was a problem hiding this comment.
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 }} { |
There was a problem hiding this comment.
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.
{% for field in variant.fields() %} | ||
{{ field.name()|var_name_swift }}: {{ field.type_()|type_swift }}{%- if loop.last -%}{%- else -%},{%- endif -%} | ||
{% endfor %} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"); | ||
} |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() %} |
There was a problem hiding this comment.
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.
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 }}( |
There was a problem hiding this comment.
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 }}() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the same vein:
{%- endif %}) : {{ e.name()|class_name_kt }}() { | |
: {{ e.name()|class_name_kt }}() { {%- endif %}) |
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 | ||
} |
There was a problem hiding this comment.
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:
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()
}
There was a problem hiding this comment.
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!!") |
There was a problem hiding this comment.
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.)
Updated in response to @jhugman latest feedback. |
|
||
val var1: EnumerationAvecDonnees = EnumerationAvecDonnees.Zero | ||
val var2: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(1u) | ||
val var3: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(2u) |
There was a problem hiding this comment.
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
.
Hrm, the
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. |
There was a problem hiding this 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 %} |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
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() | ||
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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, ...); } ```
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:
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