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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 with associated data (aka "tagged unions" or "sum types").
* C-style structs containing named fields (we call these *records*).
* Sequences of all of the above (like Rust's `Vec<T>`).
* Optional instances of all of the above (like Rust's `Option<T>`).
Expand All @@ -98,7 +98,6 @@ Things that are implemented so far:

Things that are not implemented yet:

* Enums with associated data.
* Union types.
* Efficient access to binary data (like Rust's `Vec<u8>`).
* Passing object references to functions or methods.
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/src/internals/lifting_and_lowering.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Calling this function from foreign language code involves the following steps:
| `T?` | `RustBuffer` struct pointing to serialized bytes |
| `sequence<T>` | `RustBuffer` struct pointing to serialized bytes |
| `record<DOMString, T>` | `RustBuffer` struct pointing to serialized bytes |
| `enum` | `uint32_t` indicating variant, numbered in declaration order starting from 1 |
| `enum` and `[Enum] interface` | `RustBuffer` struct pointing to serialized bytes |
| `dictionary` | `RustBuffer` struct pointing to serialized bytes |
| `interface` | `uint64_t` opaque integer handle |

Expand All @@ -84,7 +84,7 @@ The details of this format are internal only and may change between versions of
| `T?` | If null, serialized `boolean` false; if non-null, serialized `boolean` true followed by serialized `T` |
| `sequence<T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `T` |
| `record<DOMString, 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 |
| `enum` and `[Enum] interface` | Serialized `i32` indicating variant, numbered in declaration order starting from 1, followed by the serialized values of the variant's fields in declaration order |
| `dictionary` | The serialized value of each field, in declaration order |
| `interface` | *Cannot currently be serialized* |

Expand Down
24 changes: 23 additions & 1 deletion docs/manual/src/udl/enumerations.md
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,
Expand All @@ -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);
};
```
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


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" :)

11 changes: 10 additions & 1 deletion examples/rondpoint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ pub enum Enumeration {
Trois,
}

#[derive(Debug, Clone)]
pub enum EnumerationAvecDonnees {
Zero,
Un { premier: u32 },
Deux { premier: u32, second: String },
}

#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
pub struct minusculeMAJUSCULEDict {
Expand All @@ -54,7 +61,9 @@ fn copie_enumerations(e: Vec<Enumeration>) -> Vec<Enumeration> {
e
}

fn copie_carte(e: HashMap<String, Enumeration>) -> HashMap<String, Enumeration> {
fn copie_carte(
e: HashMap<String, EnumerationAvecDonnees>,
) -> HashMap<String, EnumerationAvecDonnees> {
e
}

Expand Down
9 changes: 8 additions & 1 deletion examples/rondpoint/src/rondpoint.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

boolean switcheroo(boolean b);
};

Expand All @@ -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;
Expand Down
19 changes: 18 additions & 1 deletion examples/rondpoint/tests/bindings/test_rondpoint.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
))
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 👍


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.

assert(var1 != var2)
assert(var2 != var3)
assert(var1 == EnumerationAvecDonnees.Zero)
assert(var1 != EnumerationAvecDonnees.Un(1u))
assert(var2 == EnumerationAvecDonnees.Un(1u))

assert(switcheroo(false))

Expand Down
24 changes: 18 additions & 6 deletions examples/rondpoint/tests/bindings/test_rondpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,32 @@

assert copie_enumeration(Enumeration.DEUX) == Enumeration.DEUX
assert copie_enumerations([Enumeration.UN, Enumeration.DEUX]) == [Enumeration.UN, Enumeration.DEUX]
assert copie_carte({"1": Enumeration.UN, "2": Enumeration.DEUX}) == {"1": Enumeration.UN, "2": Enumeration.DEUX}
assert copie_carte({
"0": EnumerationAvecDonnees.ZERO(),
"1": EnumerationAvecDonnees.UN(1),
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
}) == {
"0": EnumerationAvecDonnees.ZERO(),
"1": EnumerationAvecDonnees.UN(1),
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
}

assert switcheroo(False) is True

assert EnumerationAvecDonnees.ZERO() != EnumerationAvecDonnees.UN(1)
assert EnumerationAvecDonnees.UN(1) == EnumerationAvecDonnees.UN(1)
assert EnumerationAvecDonnees.UN(1) != EnumerationAvecDonnees.UN(2)

# Test the roundtrip across the FFI.
# This shows that the values we send come back in exactly the same state as we sent them.
# i.e. it shows that lowering from python and lifting into rust is symmetrical with
# lowering from rust and lifting into python.
rt = Retourneur()

def affirmAllerRetour(vals, identique):
for v in vals:
id_v = identique(v)
assert id_v == v, f"Round-trip failure: {v} => {id_v}"
for v in vals:
id_v = identique(v)
assert id_v == v, f"Round-trip failure: {v} => {id_v}"

MIN_I8 = -1 * 2**7
MAX_I8 = 2**7 - 1
Expand Down Expand Up @@ -87,8 +99,8 @@ def affirmAllerRetour(vals, identique):

def affirmEnchaine(vals, toString, rustyStringify=lambda v: str(v).lower()):
for v in vals:
str_v = toString(v)
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
str_v = toString(v)
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"

# Test the efficacy of the string transport from rust. If this fails, but everything else
# works, then things are very weird.
Expand Down
15 changes: 14 additions & 1 deletion examples/rondpoint/tests/bindings/test_rondpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ assert(dico == copyDico)

assert(copieEnumeration(e: .deux) == .deux)
assert(copieEnumerations(e: [.un, .deux]) == [.un, .deux])
assert(copieCarte(c: ["1": .un, "2": .deux]) == ["1": .un, "2": .deux])
assert(copieCarte(c:
["0": .zero,
"1": .un(premier: 1),
"2": .deux(premier: 2, second: "deux")
]) == [
"0": .zero,
"1": .un(premier: 1),
"2": .deux(premier: 2, second: "deux")
])

assert(EnumerationAvecDonnees.zero != EnumerationAvecDonnees.un(premier: 1))
assert(EnumerationAvecDonnees.un(premier: 1) == EnumerationAvecDonnees.un(premier: 1))
assert(EnumerationAvecDonnees.un(premier: 1) != EnumerationAvecDonnees.un(premier: 2))


assert(switcheroo(b: false))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}> {
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.

[[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:
Expand All @@ -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() -%}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ dictionary {{ rec.name()|class_name_webidl(context) }} {
{% endfor %}

{%- for e in ci.iter_enum_definitions() %}
{% if ! e.is_flat() %}
// Sorry the gecko-js backend does not yet support enums with associated data,
// so this probably isn't going to compile just yet...
{% endif %}
enum {{ e.name()|class_name_webidl(context) }} {
{% for variant in e.variants() %}
"{{ variant|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
"{{ variant.name()|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
{% endfor %}
};
{% endfor %}
Expand Down
85 changes: 77 additions & 8 deletions uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt
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.
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.

#}

{% 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 %}
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.

Loading