-
Notifications
You must be signed in to change notification settings - Fork 238
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
# Enumerations | ||
|
||
An enumeration defined in Rust code as | ||
|
||
```rust | ||
enum Animal { | ||
Dog, | ||
|
@@ -17,4 +18,25 @@ enum Animal { | |
}; | ||
``` | ||
|
||
Note that enumerations with associated data are not yet supported. | ||
Enumerations with associated data require a different syntax, | ||
due to the limitations of using WebIDL as the basis for UniFFI's interface language. | ||
An enum like this in Rust: | ||
|
||
```rust | ||
enum IpAddr { | ||
V4 {q1: u8, q2: u8, q3: u8, q4: u8}, | ||
V6 {addr: string}, | ||
} | ||
``` | ||
|
||
Can be exposed in the UDL file with: | ||
|
||
```idl | ||
[Enum] | ||
interface IpAddr { | ||
V4(u8 q1, u8 q2, u8 q3, u8 q4); | ||
V6(string addr); | ||
}; | ||
``` | ||
|
||
Only enums with named fields are supported by this syntax. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (to reinforce the naming thing - the docs keep calling them "enums" :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 |
||
boolean switcheroo(boolean b); | ||
}; | ||
|
||
|
@@ -20,6 +20,13 @@ enum Enumeration { | |
"Trois", | ||
}; | ||
|
||
[Enum] | ||
interface EnumerationAvecDonnees { | ||
Zero(); | ||
Un(u32 premier); | ||
Deux(u32 premier, string second); | ||
}; | ||
|
||
dictionary Dictionnaire { | ||
Enumeration un; | ||
boolean deux; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,24 @@ assert(dico == copyDico) | |
|
||
assert(copieEnumeration(Enumeration.DEUX) == Enumeration.DEUX) | ||
assert(copieEnumerations(listOf(Enumeration.UN, Enumeration.DEUX)) == listOf(Enumeration.UN, Enumeration.DEUX)) | ||
assert(copieCarte(mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX)) == mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX)) | ||
assert(copieCarte(mapOf( | ||
"0" to EnumerationAvecDonnees.Zero, | ||
"1" to EnumerationAvecDonnees.Un(1u), | ||
"2" to EnumerationAvecDonnees.Deux(2u, "deux") | ||
)) == mapOf( | ||
"0" to EnumerationAvecDonnees.Zero, | ||
"1" to EnumerationAvecDonnees.Un(1u), | ||
"2" to EnumerationAvecDonnees.Deux(2u, "deux") | ||
)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Heh, no worries at all, exactly the sort of early feedback I'm after 👍 |
||
|
||
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 commentThe 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 |
||
assert(var1 != var2) | ||
assert(var2 != var3) | ||
assert(var1 == EnumerationAvecDonnees.Zero) | ||
assert(var1 != EnumerationAvecDonnees.Un(1u)) | ||
assert(var2 == EnumerationAvecDonnees.Un(1u)) | ||
|
||
assert(switcheroo(false)) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,13 +29,17 @@ namespace dom { | |
namespace {{ context.detail_name() }} { | ||
|
||
{% for e in ci.iter_enum_definitions() %} | ||
{% if !e.is_flat() %} | ||
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data: {{ e.name() }}"); | ||
{% else %} | ||
template <> | ||
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 commentThe 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 commentThe 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. |
||
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) { | ||
auto variant = aReader.ReadInt32(); | ||
switch (variant) { | ||
{% for variant in e.variants() -%} | ||
case {{ loop.index }}: | ||
aLifted = {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}; | ||
aValue = {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}; | ||
break; | ||
{% endfor -%} | ||
default: | ||
|
@@ -45,30 +49,18 @@ struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> { | |
return true; | ||
} | ||
|
||
[[nodiscard]] static uint32_t Lower(const {{ e.name()|class_name_cpp(context) }}& aLifted) { | ||
switch (aLifted) { | ||
static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) { | ||
switch (aValue) { | ||
{% for variant in e.variants() -%} | ||
case {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}: | ||
return {{ loop.index }}; | ||
case {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}: | ||
aWriter.WriteInt32({{ loop.index }}); | ||
{% endfor -%} | ||
default: | ||
MOZ_ASSERT(false, "Unknown raw enum value"); | ||
} | ||
return 0; | ||
} | ||
}; | ||
|
||
template <> | ||
struct Serializable<{{ e.name()|class_name_cpp(context) }}> { | ||
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) { | ||
auto rawValue = aReader.ReadUInt32(); | ||
return ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lift(rawValue, aValue); | ||
} | ||
|
||
static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) { | ||
aWriter.WriteUInt32(ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lower(aValue)); | ||
} | ||
}; | ||
{% endif %} | ||
{% endfor %} | ||
|
||
{% for rec in ci.iter_record_definitions() -%} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,88 @@ | ||
{# | ||
// 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 commentThe 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 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 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 commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. |
||
#} | ||
|
||
{% if e.is_flat() %} | ||
|
||
enum class {{ e.name()|class_name_kt }} { | ||
{% for variant in e.variants() %} | ||
{{ variant|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %} | ||
{% endfor %} | ||
{% for variant in e.variants() -%} | ||
{{ variant.name()|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %} | ||
{%- endfor %} | ||
|
||
companion object { | ||
internal fun lift(n: Int) = | ||
try { values()[n - 1] } | ||
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} { | ||
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) } | ||
} | ||
|
||
internal fun read(buf: ByteBuffer) = | ||
try { values()[buf.getInt() - 1] } | ||
catch (e: IndexOutOfBoundsException) { | ||
throw RuntimeException("invalid enum value, something is very wrong!!", e) | ||
} | ||
} | ||
|
||
internal fun lower(): RustBuffer.ByValue { | ||
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)}) | ||
} | ||
|
||
internal fun write(buf: RustBufferBuilder) { | ||
buf.putInt(this.ordinal + 1) | ||
} | ||
} | ||
|
||
internal fun read(buf: ByteBuffer) = lift(buf.getInt()) | ||
{% else %} | ||
|
||
sealed class {{ e.name()|class_name_kt }} { | ||
{% for variant in e.variants() -%} | ||
{% if !variant.has_fields() -%} | ||
object {{ variant.name()|class_name_kt }} : {{ e.name()|class_name_kt }}() | ||
{% else -%} | ||
data class {{ variant.name()|class_name_kt }}( | ||
{% for field in variant.fields() -%} | ||
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %} | ||
{% endfor -%} | ||
) : {{ e.name()|class_name_kt }}() | ||
{%- endif %} | ||
{% endfor %} | ||
|
||
companion object { | ||
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} { | ||
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) } | ||
} | ||
|
||
internal fun read(buf: ByteBuffer): {{ e.name()|class_name_kt }} { | ||
return when(buf.getInt()) { | ||
{%- for variant in e.variants() %} | ||
{{ loop.index }} -> {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}{% if variant.has_fields() %}( | ||
{% for field in variant.fields() -%} | ||
{{ "buf"|read_kt(field.type_()) }}{% if loop.last %}{% else %},{% endif %} | ||
{% endfor -%} | ||
){%- endif -%} | ||
{%- endfor %} | ||
else -> throw RuntimeException("invalid enum value, something is very wrong!!") | ||
} | ||
} | ||
} | ||
|
||
internal fun lower() = this.ordinal + 1 | ||
internal fun lower(): RustBuffer.ByValue { | ||
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)}) | ||
} | ||
|
||
internal fun write(buf: RustBufferBuilder) = buf.putInt(this.lower()) | ||
internal fun write(buf: RustBufferBuilder) { | ||
when(this) { | ||
{%- for variant in e.variants() %} | ||
is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> { | ||
buf.putInt({{ loop.index }}) | ||
{% for field in variant.fields() -%} | ||
{{ "(this.{})"|format(field.name())|write_kt("buf", field.type_()) }} | ||
{% endfor -%} | ||
} | ||
{%- endfor %} | ||
}.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } | ||
} | ||
} | ||
|
||
{% endif %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole file: lovely. |
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 namedV4
rather than as a field or method namedV4
. 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: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.
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!
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.
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.
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