generate enum variant accessors#125
Conversation
This happens every time I run `cargo xtask ci`, so I committed it.
|
out of interest, I tried to also allow multi-field tuple variants, but doing that via |
|
Hi! Thanks for the PR. I think you are addressing a real problem, I thought about this problem a little, let's explore the problem space. The problem is that there is Rust's The
If I want to design a solution addressing all of these on the fly, I would add a class member for each variant into the enum, and each variant class has these static functions on it: And enum will get these static function members: These variant classes can get field access with similar trick that we use for structs. Inside them, there is only an What do you think about this? I generated this on the fly, so probably there is something I missed. And names are definitely subject of bikeshed. I would like to determine the general solution first, then if you want to implement only a part of it (e.g. for single field variants) that's fine as long as it's compatible with the big picture plan. |
|
hi! thanks for you thoughts. do I understand correctly that this syntax is nice and general, although it might look a bit more verbose, e.g. if (val.matches_Variant()) {
auto val = val.unwrap_Variant();
}turns into if (Enum::Variant::check(val)) {
auto val = enum::Variant::match(val).f0;
}but I don’t think it’s a big deal. either way, I’m happy to try implementing (some subset of) your design (probably starting with |
|
also would probably need |
|
I want to also share my thoughts on implementation so you can verify if that’s reasonable.
does that look right? if so, I’ll start implementing that. |
It can hold the
What about also providing
Makes sense.
I think you need to change the zng file as well, for annotating fields. A candidate syntax: Other than that, I think it is good for start. |
maybe, yeah, although the
it is a bit unfortunate that this would require mentioning every field twice. maybe variants could have implicit constructors based on their fields? |
|
one other interesting moment: if not every variant is declared (do we want to support this?), |
This also exists for struct. We could have a
I think we have these options:
I don't like 2 and 4, and going with 1 makes 3 in future a breaking change (breaking change isn't a hard limit, but I prefer to avoid it). |
|
the difference with structs is that enum variants are guaranteed to have all fields public. could you explain what do you mean by “couldn’t afford offsets”? I thought offsets only incur runtime cost if they’re used, and otherwise are just a single going with 3 for now is the most conservative choice probably? also while 1 makes 3 a breaking change, it still leaves 5 open (although that’s more boilerplate forever). |
Offsets were mandatory at the time, we then got runtime offsets later. So we may want to revisit this (but I still want consistency between structs and variants).
If a field is not public for a struct, the constructor is not usable.
👍 . It is not forever (yet), it just creates some barrier for adopting 1. I'm not afraid of doing breaking changes (this PR is going to be a breaking change, breaking |
yeah, and that is not a concern for enum variants, because all the fields are always public, so you can always specify every field. on the other hand, you may not be able to specify some struct field, in which case we can’t auto-generate a constructor. |
This is fine for an explicit |
|
I think implicitly generating constructors for enum variants makes sense, but if you want full consistency, I can try implementing |
|
I'm fine with implicit constructor for variants, and not touching structs in this PR. |
|
status update: making side note: is there any way to improve |
do I understand correctly that I should remove enum constructors completely, replacing them with variant declarations (rather than letting the two coexist)? |
|
status update update: made a checklist tracker in the original post. I need to do field support now, and I’m a bit intimidated by all the machinery. I definitely don’t want to duplicate all this code, so ideally it would be generic for type fields and enum variant fields. I think basically the only thing that would change between type fields and variant fields is the base pointer, right? could you point me to where this base pointer is created/passed? |
I think so.
The black magic trick is that you create an annonymous union of the data and all fields. So the field would get the same address of the struct/variant data. So you can cast |
|
okay, thanks, I’ll try adapting all this magic so it hopefully also works with variant classes without separate implementation
this is cool, but isn’t it kinda union punning between field types, which should be UB in C++?.. or is the fact that no actual memory access happens saves us here? |
I think so. The act of taking pointer to inactive members is not UB, but dereferencing that pointer is UB (which we don't). |
|
okay, so the approach used for structs unfortunately won’t work for enums, because there’re two worse versions that I see, but maybe there’s something better I’m missing:
in either case, we could probably reuse most of the logic still, as basically only offset calculation changes. also, in either case there’s seemingly no point in supporting non-auto offsets, as we have no real way of checking them in compile time (well, I guess we could if the enum implements of these two options, I think option 2 is more sensible, as it doesn’t introduce mutable state. |
8bb2cb5 to
a5c900f
Compare
|
I pushed a version that covers all the API surface you described. the tests will fail rn, since I yeeted named constructors in favour of variants, but otherwise it should be ready. this is a semi-large patch, so I pushed the implementation while I work on tests and docs. |
|
status update: porting interestingly, there’re two kinds of nonexhaustiveness:
I propose a new (alternatively, it could add a |
|
I like omitting the |
cde9d0a to
c4d4c5d
Compare
|
status update: implemented I will do book changes next, although I’m not sure it would be very soon, since I’ll be away for some time. |
|
another note: migrating examples revealed some usability papercuts. I think the most annoying by far is that when you do stuff like auto meow = Enum::Variant(42);
Enum meow = Enum::Variant(42);which is annoyingly repetitive (and it’s not intuitive that this is what you need to do from the error). I’m not sure what to do with this. one band-aid would be to provide some kind of upcast method, like some other issues:
|
|
as a wild idea, variants might inherit from the enum class instead of containing it, but that would need a different solution for fields, since we can’t put a superclass into a union. might have other problems, C++ inheritance is not something I’m very familiar with |
|
thanks! I somehow missed a test I guess, I’ll re-check |
33ee9c7 to
a38e811
Compare
I thought about this problem. The inheritance is dead end (It breaks the union trick, won't fix the lambda problem, make upcasting ambiguous, adds a vtable member to every enum, ...) but I had thought of two more solutions you didn't mention:
I concluded that
I prefer to not have methods on types, to prevent shadowing original methods.
I hate C++. Probably we can't do much here.
That's fine, and thanks for the great work on this PR! |
I think my example here hid the problem. consider: Result<Unit, ReadlineError>(Result<Unit, ReadlineError>::Err(err))this is really annoying to write, doubly so because irl it would be in some kind of namespace, requiring either a lot of the same goes for all the other free functions.
I understand this concern, but I feel like a weird enough naming convention (like the original |
|
noooo, not MSVC т_т |
|
that is really fun! I love microsoft visual studio c++ compiler! https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ hold on a sec, I’ll fix all the nmakefiles |
af9fee8 to
1dd64c3
Compare
1dd64c3 to
fc5f014
Compare
|
oh, right, this was a thing too. sorry to make you re-approve it so many times, I just don’t have MSVC locally |
No worries. It's such a dumb feature of github. |
| // REVIEW: this straight up returns u32 instead of doing the output pointer dance | ||
| // is there some reason not to do it like this? |
There was a problem hiding this comment.
It's fine when we know the output is u32. Also, we can use a simple pointer instead of a pointer to a pointer. The out pointer and pointer to pointer is for when we want to be generic over everything.
| @@ -1,6 +1,6 @@ | |||
| CXX = cl.exe | |||
| # MSVC doesn't support earlier that c++14 | |||
| CXXFLAGS = /W4 /DEBUG /EHsc /std:c++14 | |||
There was a problem hiding this comment.
Let's just add a || !defined(__cplusplus) to our guards, to not requiring a new flag for windows people, and supporting other broken compilers.
There was a problem hiding this comment.
it’s defined, it’s just set to 1997. we could check for _MSVC_LANG, which should work regardless of flags, although it’s really annoying that we have to do this.
There was a problem hiding this comment.
It doesn't make any sense. So windows people deserve this misery I guess and we shouldn't help them then.
| @@ -0,0 +1,10 @@ | |||
| # Example: Simple | |||
| non_exhaustive; | ||
|
|
||
| constructor Ok(()); | ||
| variant Ok { |
| impl CppType { | ||
| pub fn into_ref(self) -> CppType { | ||
| if self.tail.is_some() { | ||
| // TODO: this will do `Option::Some` -> `Ref<Option::Some>` conversion in the future |
There was a problem hiding this comment.
So what happens when we call match_ref currently? Does zngur panics?
There was a problem hiding this comment.
nah, this is just unreachable I think, .match_ref() doesn’t use this func. (but maybe it should, I’ll check if that’s possible)
| ) -> String { | ||
| let ZngurField { name, .. } = field; | ||
| let mn = self.mangle_name(&format!("{owner}_{variant}_field_{name}_offset")); | ||
| // REVIEW: what should we do if not matched? this should not happen, |
There was a problem hiding this comment.
We control this and nothing bad could call it, right? If so, do a unsafe { unreachable_unchecked() } which helps the optimizer optimize this function to a single constant.
There was a problem hiding this comment.
yeah, I’m just reluctant to introduce UB like that, so I did an abort for now. I could do unreachable_unchecked if you prefer that, we do control this and this should be statically prevented from happening.
There was a problem hiding this comment.
We will get the UB anyway when we move to offsetof!, so I think the risk of UB here is justified.
|
|
||
| #if __cplusplus >= 201703L | ||
| {% if let Some(discriminant) = td.discriminant %} | ||
| // REVIEW: you wrote it should be a static method, but I took the liberty of making it a normal method instead. |
There was a problem hiding this comment.
Yes I meant a normal method. match_ref and match_mut are not keywords, but they aren't there?
There was a problem hiding this comment.
I'm fine with those being a normal method as well. My requirements are a bit fuzzy :). Also, using static methods won't help in avoiding name conflict since C++ is garbage static methods are also available as obj.method syntax. Function overloading helps us here, but relying on function overloading for preventing conflicts makes things confusing.
|
|
||
| #if __cplusplus >= 201703L | ||
| {% if let Some(discriminant) = td.discriminant %} | ||
| // REVIEW: it could be also called `match` in principle? |
There was a problem hiding this comment.
You mean calling both functions match by function overloading? Then we would lose Enum e; e.match_mut() but the conflict prevention is nice.
There was a problem hiding this comment.
it’s not really function overloading I guess, because the methods are defined on distinct types, but yeah, that does lose this, and it might be to big of an ergonomic hit
There was a problem hiding this comment.
So let's keep them as is (I think Enum e; e.match_mut() already doesn't work in this PR, please add it and use it in tests).
(imagine it’s a tracking issue for what was proposed in this comment now)
variant classes:
enum class:
original text
hi! currently, to use rust enums in cpp you have to manually write and bind functions that extract data from enum variants. this patch adds automatic generation of
.unwrap_Variant()methods for the trivial case: tuple variants that only have a single fieldthis restriction avoids the question of how the return type should look for more complex cases: I think for tuple variants with multiple fields we should return
std::tuple, and for struct variants we should codegen a helper struct, but in these cases there’re multiple options, and a single-field tuple variant case is straightforward. at the same time, this has a lot of utility, as this case is really common, and all the other cases can be emulated by putting a struct inside the single field(unfortunately, this also generates an unwrap method for single-field tuple structs, as zng format does not distinguish between structs and enums. I could gate the generation on having multiple constructors though, let me know what you think)
obviously, this will also need tests and documentation, which I’m happy to write if you accept the idea in general