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

enum with values / distinct const #1129

Open
lerno opened this issue Feb 13, 2024 · 77 comments
Open

enum with values / distinct const #1129

lerno opened this issue Feb 13, 2024 · 77 comments
Labels
Discussion needed This feature needs discussion to iron out details Enhancement Request New feature or request

Comments

@lerno
Copy link
Collaborator

lerno commented Feb 13, 2024

Enums changed with #428 from classic C enums to ordinal based enums.

This left a hole in the language, not the least in how to define C enums that aren't strict ordinals.

Early ideas included an attribute – which is bad because an attribute should be affecting the entire implementation of the enum. As well as just work around it with distinct type + sub module.

Later distinct was proposed for this, but worked poorly as it was not a keyword at the time. However, currently something like

module baz;
distinct const Foo : int
{
  ABC = 3,
  BCE = 123
}

Could be considered.

Questions remain in regards to semantic and usage. For example, is the usage: baz::ABC or Foo.ABC or Foo::ABC. The first case considers the code mere shorthand for:

module baz;
distinct Foo = inline int;
Foo ABC = 3;
Foo BCE = 123;

The second is close to enum style, but the question is then whether this is desirable giving the difference in semantics. The final variant Foo::ABC would give it a unique look, clearly indicating it is from a "const set", but then unlike Foo.ABC with the obvious inference of .ABC it would not seem like it should implement inference.

distinct const while not requiring a new set of keywords is fairly long, and the question is whether this is good.

Other syntactic alternatives would be:

constset Foo1 : int
{ 
  ABC = 3,
  BCE = 123
}
const enum Foo2 : int
{  
  ABC = 3,
  BCE = 123
}
enumconst Foo3 : int
{  
  ABC = 3,
  BCE = 123
}

All of those suppose a separate kind of types. The initial option with baz::ABC retains the option of just modelling this as constants.

@cbuttner
Copy link
Contributor

Just some thoughts.
A question to ask is whether this would just be some syntactic sugar where it just wraps come constants in a namespace, or would it pull some more weight.

Would there be auto-incrementing like C enums?
Would it be a new kind of type for reflection purposes, where I can get an array of all the constants? If so, it seems to me that distinct const might not be a very expressive or unique name. constset could be the right idea here.
I'm also noticing that distinct const implies the existence of def const.

With foo::ABC we almost have classic C enums, but how would that help with naming collisions within the same module. Foo.ABC is more consistent with "accessing something from a type", but Foo::ABC is indeed more distinct which could also have benefits. Kind of has that "constants in a module/namespace" feel to it. I don't see why it couldn't do inference in either case, I doubt there are any expectations that only enums should do that. After all module paths are also inferred.

@lerno
Copy link
Collaborator Author

lerno commented Feb 14, 2024

Auto-incrementation could happen, although I don't see all that much use for it to be honest. When you're using values that's usually what you want for everything, barring the case where you're reserving 0

It could be possible that you could get a list of all the constants in some way using reflection, but there are nuances here. It's obviously not possible to do a name <=> value mapping, so at compile time, what is passed around if we get a compile time reference to such a thing? Like for enums we could provide a list of all the names and a list for all the values. Such a thing could exist both at compile time and runtime without any problems as long as it isn't extensible (i.e. possible to add more constants at a later time)

One thing to note is how such compile time capabilities interact with the forward/backwards compatibility of enums.

It is common in frameworks to see the equivalent of this:

// Framework 5.0
enum Foo
{
  FOO_ONE = 3,
  FOO_TWO = 100,
 } 
 // Framework 6.0
 enum Foo
 {
   FOO_ONE = 3,
   FOO_TEST = 100,
   FOO_TWO = FOO_TEST @deprecated("use FOO_TEST"),
   FOO_HELLO = -10239
 }

If we think about how this interacts with an application that bound to the 5.0 framework.
If the app encoded the list of values somewhere, it will see -10239 and not recognize it. This is fine if it's a switch. Typically you'd add a default. But what if code was like this:

Foo f = get_foo_from_system();
String[] all_foos = Foo.names(); // Compile time generated from 5.0
int[] all_foo_values = Foo.values(); // Compile time generated from 5.0
foreach (i, val : all_foo_values) 
{
   if (val == f) return all_foos[i];
}
return "Invalid foo"

So some care has to be taken as to what the guarantees are and so on. This is similar for enums, but may be more surprising for a user in this case.

Regarding name collisions foo::ABC would not prevent same module collisions, but in the case where it's replicating a C API the constant would be namespaced anyway.

The argument against inference for Foo::ABC is that it would somewhat violate expectations. But yes, it could be supported.

@cbuttner
Copy link
Contributor

Yeah, good points. Now I'm questioning what else reflection would even be useful for.

@lerno
Copy link
Collaborator Author

lerno commented Feb 14, 2024

I think it's mostly automating debug things.

@lerno
Copy link
Collaborator Author

lerno commented Feb 14, 2024

Possibly some mapping.

@lerno lerno added this to the 0.5.5 milestone Feb 15, 2024
@lerno lerno added the Enhancement Request New feature or request label Feb 15, 2024
@data-man
Copy link
Contributor

data-man commented Feb 17, 2024

This is almost the most awaited feature for me, thank you! :)
Some thoughts:

  1. OrdinalEnum or OrdEnum.
  2. Support simple expressions (or even macros) in enum's fields. This will make porting from other languages easier.
  3. Less restrictive naming - allow mixed case in field names. Also to make porting easier.

In addition to point 1: what if ordinal enum becomes the default enum and current enum will be renamed to EnumExt or something similar?
Or make enum more universal and let the compiler decide which enum is declared.

@lerno
Copy link
Collaborator Author

lerno commented Feb 23, 2024

I tentatively tried implementing this, but as far as I can tell, the benefit seems small over enums with associated values:

constset Foo : int
{
  ABC = 3,
  BCE = 123
}

enum Bar : int(int val)
{
   ABC(3),
   BCE(123)
}

// With constset
extern fn void some_c_foo(Foo f); 

// With enum + associated value
extern fn void _some_c_bar(int val) @extern(some_c_bar) @local;
macro void some_c_bar(Bar bar) => _some_c_bar(bar.val);

@lerno
Copy link
Collaborator Author

lerno commented Feb 23, 2024

The advantage is somewhat greater when going the other direction:

// With constset
extern fn Foo c_foo(); 
...
switch (c_foo())
{
	case Foo.ABC: ...
	case Foo.BCE: ...
	...
}

// With enum + associated value
extern fn int c_bar();
...
switch (c_bar())
{
	case Bar.ABC.val: ...
	case Bar.BCE.val: ...
}

@lerno
Copy link
Collaborator Author

lerno commented Feb 23, 2024

We could imagine supercharging associated values, in which case we'd get this:

enum Bar : int(inline int val)
{
   ABC(3),
   BCE(123)
}
extern fn void some_c_bar(int val);
...
some_c_bar(Bar.ABC);

However this loses some type safety. Which might be acceptable though.

@lerno
Copy link
Collaborator Author

lerno commented Feb 23, 2024

But we could go further with this, although at a greater cost to the grammar:

// Imagine the associated type can be used as a pseudo-type:
extern fn void some_c_bar(Bar.val val);
...
some_c_bar(ABC);

This is much more complex to handle, but still somewhat possible. If we think about it from an "ease of learning", I think this might be going overboard too. Unless we have usecases that are not when interfacing with C.

@lerno
Copy link
Collaborator Author

lerno commented Feb 23, 2024

It's fairly straightforward to construct a macro @enum_by_value that would do something like:

extern fn int _c_bar() @extern("c_bar") @local;
macro Bar c_bar() => @enum_by_value(Bar, val, c_bar());

Which then would convert an int to Bar like the constset version without any extra magic.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

To add a bit to this, why do I say that it's "complex"? Well, for any variant of this that is anything but a convenient way of creating a distinct type + a list of constants for this type we require a new kind of type. This type must be checked for correct semantics everywhere, which means it adds to the general complexity of the semantic checker in a negative way. If it's merely a shorthand for:

module baz;
distinct Foo = inline int;
Foo ABC = 3;
Foo BCE = 123;

Then we're fine, because no extra type is needed, but then we don't get Foo.ABC or Foo::ABC either!

This is why I bring up the associated values for enums. They are incredibly useful in some cases, but largely they seem overlooked. While they could be removed from the language, there isn't actually that much extra complexity in supporting the feature itself.

So if this feature could accommodate the "distinct const" use-case as well, that would be a double win.

I've already outlined some way it could be done, but to add one more:

enum Bar : int(inline int val)
{
   ABC(3),
   BCE(123)
}
extern fn void some_c_bar(inline Bar b);
...
some_c_bar(ABC);

Now the way this will work from the language's perspective is this:

  1. When it sees "inline Bar", it understands that the actual underlying type should be the inline parameter of Bar, so in this case int. There is no restriction here by the way. It could equally well be some struct.
  2. When passed an argument, the type check will be against Bar however.

The somewhat confusing result though is that the type of b here isn't the type of Bar, but rather the type of the inline parameter.

Rather than using inline this could be using attributes:

extern fn void some_c_bar(Bar b @access(val));

Such a feature could in that case be more generally applied, and possible to use with any type. It is less obvious though.

Without attribute it could look something like:

extern fn void some_c_bar(Bar.val b);

although this can certainly run into some grammar difficulties.

It should also be noted that something like @access and inline can be more generally applied, e.g:

// This macro would then work on any enum with an inline parameter
macro void abc(inline b)
{ ... }
// This macro could extract "val" from any type:
macro void abc(b @access(val))
{ ... }

Another thing in favour of `@access` though is that we typically use these only for external functions:
```c
// Today
extern fn void _some_c_bar(int b) @inline @local;
fn void some_c_bar(Bar b) => some_c_bar(b.val);

// Access idea
extern fn void some_c_bar(Bar b @access(val));

@cbuttner
Copy link
Contributor

Even though this:

module baz;
distinct Foo = inline int;
const Foo ABC = 3;
const Foo BCE = 123;

is technically a solution, it has the following issues:

  • If I'm generating docs this will show up as a separate module unless I'm somehow overriding it, as opposed to being part of a larger module it's supposed to "belong to".
  • If I'm generating headers and for example use the strategy of one file per module by default, this will add a new header file just a few constants.
  • If I'm adding this as part of a larger module inside a file, It has to be at the end or beginning, or you have to declare the larger module and imports again.

So to me this is a granularity/module fragmentation issue. I'd rather have Foo be part of a larger module than having to open a module just for a few constants.

Instead what I'm doing right now is just prefix the names as you would do in C:

distinct Foo = inline int;
const Foo FOO_ABC = 3;
const Foo FOO_BCE = 123;

Which has the (minor) problem that you still have to qualify with at least one module when using the constants from another module so the names get even longer.
So the original proposal of

distinct const Foo : inline int
{
  ABC = 3,
  BCE = 123
}

for shortening names and usage to Foo::ABC or Foo.ABC makes sense to me at the moment. It would also not require a new type and consequently keep complexity low.

@cbuttner
Copy link
Contributor

The enums with associated values is interesting as well, but it looks less intuitive, and it's usefulness seems to be limited to C interop.

It's potentially more error prone too, since you might be tempted to cast the enum to int to get its real value instead of using foo.val.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

Unfortunately Foo::ABC and Foo.ABC both require modeling it as a separate type.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

@cbuttner Associated values as such has been in the language from the beginning and is very useful for certain use-cases. So the problem here is more whether we can leverage them.

I think the question you point out is valid: what happens with (int)myfoo? Currently, this is shorthand for myfoo.ordinal. If this change with "inline" is used however, it might be useful to entirely disallow casts on enums and strictly require . access. So rather than (int)myfoo always require myfoo.ordinal, which could get shortened to myfoo.ord.

In that case there is no confusion as to what casts means, since they aren't used.

@data-man
Copy link
Contributor

the benefit seems small over enums with associated values

Benefit №1:

enum Foo : int
{
  A = 3,
  B, // B == 4 here
  C = A + 10,
  D = 1 << 4,
  E = 1 << 5,
}

Benefit №2:
As I see in asm output, enums' values are now accessed by pointer. Correct me if I am wrong.
I think their direct values should be used.

Also:
enum Bar : int(int val)

What exactly the first int here? It's confused.

Associated values as such has been in the language from the beginning

Previously, there were the usual normal enumerations. :)

And sorry, I'm against new keywords or complicating syntax.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

Benefit №1:
Is the automatic numbering here of some essential use? I don't see it to be honest

enum Foo : int(int val)
{
  A(3),
  B(4),
  C(A.val + 10),
  D(1 << 4),
  E(1 << 5),
}

As I see in asm output, enums' values are now accessed by pointer. Correct me if I am wrong.
I think their direct values should be used.

When we're talking about the associated value yes. It is because the implementation is essentially one of:

int[5] Foo_val = { 3, 4, 13, 16, 32 };

So that retrieving the mapping is one of indexing into this variable. If you do something like

int x = Foo.A.val;

Then this will actually automatically get folded into int x = 3;.The only time the array is actually used is if you need the lookup at runtime. E.g.

Foo a = get_foo();
int x = a.val;

enum Bar : int(int val)
What exactly the first int here? It's confused.

The first int is the storage type of the enum.

Previously, there were the usual normal enumerations. :)

Associated values were always there, they just costed more (a function call was needed) and had more problems (how do you get the name of an enum from a number when that number matches two values?)

So even if we say "let's go back to C enums", that would also mean that everything you get for the enums today:
enum sets, runtime and compile time name reflection, associated values etc – they all have to go away.

And conversely, if we say "let's add C style enums too", they have exactly the problem of having to be yet another type to be handled in the type system. There is no free lunch here, just trade offs.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

Note here that we could add this:

enum Foo : int(int val)
{
   ABC = 1,
   BCD = 3,
}

As mere syntax sugar for

enum Foo : int(int val)
{
   ABC(1),
   BCD(3),
}

I would agree that the ABC(1) form leaves something to be desired visually, although it's fairly consistent.

That said, this would be consistent as well:

enum Foo : int { int val, double x }
{
   ABC = { 1, 0.0 },
   BCD = { 3, 0.4 },
}

Such a form could perhaps more naturally have the shorthand

enum Foo : int { int val }
{
   ABC = 1,
   BCD = 3,
}

@data-man
Copy link
Contributor

What about this:

C-style enum:

enum Foo : int
{
   ABC = 1,
   BCD = 3,
   EFG
}

Associated enum:

enum Foo : ( int val, double x )
{
   ABC { 1, 0.0 },
   BCD { 3, 0.4 }
}

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

As I said, keeping both C style enums and the regular ones is going to have a prohibitively high complexity cost, so this would need to be implemented as a type of enum with associated values, but where there is some convenience to let it be used similar to C enums.

@data-man
Copy link
Contributor

Ok. Let's remove associated enums completely. They can be replaced by structs:

import std::io;

struct Value
{
    int a;
    double d;
}

struct Foo
{
    Value a;
    Value b;
}

fn void main()
{
    const Foo FOO = { {1, 0}, {2, 2} };
	io::printfn("FOO.a.a %s", FOO.a.a);
	io::printfn("FOO.a.d %s", FOO.a.d);
	io::printfn("FOO.b.a %s", FOO.b.a);
	io::printfn("FOO.b.d %s", FOO.b.d);
}

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

No they can't that doesn't make sense even. There is no "associated enum" either. There's just enums backed by ordinals and enums that are glorified constants i.e. C enums.

The point is that ordinal enums support:

  1. Proper exhaustive switching
  2. Error checking on "invalid" values
  3. Runtime reflection
  4. Compile time reflection
  5. Inference

They are not getting removed, so please stop suggesting this over and over again.

It just happens that associated values are easily implemented by ordinal based enums.

For some reason you seem to insist that ordinal enums are like Swifts so-called "enums", which are actually just tagged unions but with broken naming. The ordinal enums have zero to do with tagged unions.

Whether ordinal enums have associated values or not is entirely separate to whether enums should be ordinal based or just random constants.

So again, let me state in no unclear terms that the ordinal enums are not being removed. Nor will they be considered a special case of enums. They are the normal form of enums and that is final.

What this issue is discussing, is how to support "enums as a collection of random constants" which C also allows.

From trying to implement it, it's 100% clear that anything beyond the "syntax sugar for constants" solution will require adding a new kind of type to the language. This is something that I found prohibitively costly. For that reason I am trying to find a way for "enum as a collection of random constants" to piggyback on ordinal enum. Using the associated values seems like an easy way to do this.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

enum Foo : int { int val }
{
   ABC = { 1 },
   BCD = { 3 },
}

Is ambiguous, so some alternatives are

enum Foo : { int val } int
{
   ABC = { 1 },
   BCD = { 3 },
}

enum Foo (int val) int
{
   ABC = { 1 },
   BCD = { 3 },
}

Which have the advantage of allowing eliding the ordinal storage type, e.g.

enum Foo : { int val }
{
   ABC = { 1 },
   BCD = { 3 },
}

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

Here is a real example of an enum with associated value:

enum Dir : char(int[<2>] dir)
{
  NORTH({  0, -1 }),
  EAST ({  1,  0 }),
  SOUTH({  0,  1 }),
  WEST ({ -1,  0 }),
}

We could imagine it being:

enum Dir : { int[<2>] dir } char
{
  NORTH = {  0, -1 },
  EAST  = {  1,  0 },
  SOUTH = {  0,  1 },
  WEST  = { -1,  0 },
}


Dir x = ...
location += x.dir;

@cbuttner
Copy link
Contributor

cbuttner commented Feb 24, 2024

Unfortunately Foo::ABC and Foo.ABC both require modeling it as a separate type.

Ah okay, I didn't know the cost was this high. So what's left is the original idea about the syntax sugar. That would still be an improvement since it makes a bundle of related constants more obvious to a reader, and external tools can parse it as a bundle too.

I'm not against leveraging enums either. It's clear that runtime table lookups and accessing values with .val doesn't make it a one-to-one replacement for constants, but the use cases are still visible.

I'm not sure about the inline idea though, it seems kinda quirky to me and not explicit enough. The @access one is also lacking some clarity about the involved types. How about something like this instead?

extern fn void some_c_bar(int val @from(Bar));
// Or
extern fn void some_c_bar(int foo @from(Bar.val));

Now that looks like a special case of parameter destructuring to me. Might still be too quirky though, I don't know, I leave that up to you.

@lerno
Copy link
Collaborator Author

lerno commented Feb 24, 2024

The feature is kind of hard to judge the scope of to be honest: regardless of what syntax is used, this is the kind of feature you then could leverage for other types as well. It's a very niche feature, but at the same time fairly complex in the possible uses.

If we consider something like extern fn void some_c_bar(int val @from(Bar));, would this then mean that you can use it both with an integer and a "Bar" value for example? Is it applicable also on structs? If not, then why not?

There's a reason for the "inline inline" feature:

Substructs allows something like:

struct Abc
{
  inline Baz b;
  int x;
}

To be used both where Baz and where Abc is wanted. Essentially it will implicitly create a .b access where needed.

If we just wanted extern fn void some_c_bar(int val) to work, then enum Bar : int(inline int val) would do that. However, you could also use the normal int. That is, there is no type-checking happening.

The idea with

extern fn void some_c_bar(inline Bar val);

Was to then to essentially say that "you have to use a Bar, but it will actually pass on the inlined value of Bar, rather than the ordinal"

@data-man
Copy link
Contributor

data-man commented Mar 7, 2024

Odin, Zig.

@lerno
Copy link
Collaborator Author

lerno commented Mar 7, 2024

Because you either have to have to different types under the same name, or you drop the functionality of the ordinal enums, such as runtime reflection. What language allows both and has name reflection like C3's for enums? How would that even work with enums-as-masks? It can't.

@data-man
Copy link
Contributor

data-man commented Mar 7, 2024

Masks are useful in C, because C hasn't introspection. C3 has enumset in std. It's enough.

@lerno
Copy link
Collaborator Author

lerno commented Mar 7, 2024

Odin: keeps an array of name + value, taking the name at runtime means walking through the values and trying to match them. This doesn't work for masks or overlapping values. If Odin had associated values, it would similarly be a loop for every associated value to retrieve, that is O(n) rather than O(1) time. Which would make associated values expensive enough to never get used.

Zig: creates a function with a switch, matching value to name. Again, doesn't work with masks. Overlapping values are forbidden for Zig enums. If they had associated values, those would be O(n).

@lerno
Copy link
Collaborator Author

lerno commented Mar 7, 2024

Masks are useful in C, because C hasn't introspection. C3 has enumset in std. It's enough.

Interop with C is the only reason why non-ordinal enums in this issue is even brought up.

@OdnetninI
Copy link
Contributor

After all this discussion, I can agree that non-ordinal enums are not stricly necessary.
However, I think that, from a code reading/writting perspective they are better than the repetitive sequence of const int CONST = n;.
But now, I am more inclined to not have them into the language and work with consts.
I need to see how this would affect a real medium-size application...

@lerno
Copy link
Collaborator Author

lerno commented Mar 7, 2024

The code reading is what I'm concerned about myself, and I was kind of happy to have found a way to leverage associated values in a good way. However, in the end that feature increasingly seems like a bad idea and I will remove it.

@lerno lerno removed this from the 0.6.0 milestone Mar 26, 2024
@lerno lerno added the Discussion needed This feature needs discussion to iron out details label Mar 26, 2024
@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

Ok, so another try, what if we do this:

enum Foo : inline int(int val)
{
   ABC = 1,
   BDF = 2,
}
// For this enum, it will implicitly convert to
// the ordinal:
int a = Foo.ABC; // a = 0

We can also inline a parameter:

enum Bar : int(inline int val)
{
   ABC = 1,
   BDF = 2,
}
// For this enum, it will implicitly convert to
// the ordinal:
int a = Bar.ABC; // a = 1

To avoid confusion, this will change the meaning of (int)Bar.ABC. With inline, this will be as if (int)Bar.ABC.val. If we have an enum without inline, it can't be cast:

enum Baz { FOO, BAR };
int a = (int)Baz.FOO; // ERROR

Instead it's required to use .ordinal (possibly shortened to .ord):

enum Baz { FOO, BAR };
int a = Baz.FOO.ordinal; // This is the way

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

One further comment here is that this would strengthen the enum type to be stronger than a distinct and more in line with a struct.

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

So how do we convert from an int to an enum? Again, we can't use the cast, that would lead to unwanted results, consider:

int a = Bar.ABC; // a = 1
Bar b = (Bar)a; // b = Bar.BDF !

So we need to introduce a conversion function:

Bar b = Bar.of_ordinal(1); // Bar.BDF

This one can then also be typed with the index type (e.g if the ordinal type is ichar, then of_ordinal would take an ichar).

@OdnetninI
Copy link
Contributor

So, depending where you put the inline keyword, you change what value is get from the enum implicitly.
I like that approach.

But about your last comment, casting could lead to unwanted results if you inline val instead of the enum ordinal.

What happens with repeated values? Are they valid?

enum Bar : int(inline int val)
{
   ABC = 1,
   BDF = 2,
   EHG = 2,
}

bool equal = Bar.BDF == Bar.EHG; // I would guess true....

Also, not only a of_ordinal but what if I want to get the Bar from the value:

Bar b = Bar.of_value(1); // It should be Bar.ABC

Bar c = Bar.of_value(2); // Bar.BDF or Bar.EHG ??? Any should be valid

This is important because when reading protocols from the network, you may want to express the types with an enum.
I know this is possible, but you will have to repeat it with each enum you use:

enum Bar : int(inline int val)
{
   UNK = 0,
   ABC = 1,
   BDF = 2,
   EHG = 2,
}

fn Bar getBarFromValue(int val) {
  switch(val) {
    case Bar.ABC: return Bar.ABC;
    case Bar.BDF: return Bar.BDF;
    default: return Bar.UNK;
  }
}

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

bool equal = Bar.BDF == Bar.EHG; // I would guess true....

No, this is not true. Unconverted it is a comparison of ordinals. This would be true though:bool equal = (int)Bar.BDF == (int)Bar.EHG;

Bar b = Bar.of_value(1);

I feel this has a very limited use. I don't mind writing a macro for it. It would need to look something like:
macro @enum_by(#val, value) and it would just loop through the definitions. @enum_by(Bar.val, 1). This would work for all parameters, not just the inline one.

@OdnetninI
Copy link
Contributor

No, this is not true. Unconverted it is a comparison of ordinals. This would be true though:bool equal = (int)Bar.BDF == (int)Bar.EHG;

Okay 👍

I feel this has a very limited use. I don't mind writing a macro for it. It would need to look something like: macro @enum_by(#val, value) and it would just loop through the definitions. @enum_by(Bar.val, 1). This would work for all parameters, not just the inline one.

I was thinking more on a lookup table... but nvm.
At least, let's add the macro, so the user have an easy way to convert from values to enums, and if they need performance, they can create the lookup table themselves.

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

Note that inline allows for it to inline to ANY sort of type:

enum Foo : (inline String val)
{
  ABC = "Hello",
  BCD = "World"
}
// ...
io::printfn("%s %s", (String)Foo.ABC, (String)Foo.BCD);

What this means is that the comparison could be arbitrarily complex or even undefined. So it's not clear that it is suitable for a lookup table.

@OdnetninI
Copy link
Contributor

Yeah, I still had in my mind that enum are for integers only, but this is much more powerful.

Yeah, I think the current proposal could work fine for most cases.

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

I want to highlight something however, this doesn't change the problem it's supposed to fix, namely C interfaces.

// C interface
enum Foo { FOO_ABC = 123 };
void fooSomething(enum Foo);

"Rejected" C interface:

enum Foo : (int val) { ABC = 123 }
extern fn void fooSomething(int val);
// call
fooSomething(Foo.ABC.val);

Other rejected C interface:

distinct Foo = int;
const Foo FOO_ABC = 123;
extern fn void fooSomething(Foo val);

The actual proposal

enum Foo : (inline int val) { FOO_ABC = 123 }
extern fn void fooSomething(int val); // <- int, not Foo!

We can improve it:

distinct FooVal = int;
enum Foo : (inline FooVal val) { FOO_ABC = 123 }
extern fn void fooSomething(FooVal val);

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

So really, we come back full circle and failed to do anything. Rather, it leads to reconsideration of:

  1. A completely new type
constset Foo : int
{
   ABC = 123,
}
extern fn void fooSomething(Foo val);
  1. In site .val expansion:
enum Foo : (int val) { ABC = 123 }
extern fn void fooSomething(Foo.val x);
// call
fooSomething(ABC); // implicit .val

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

Other possible ways for expansion syntax:

extern fn void fooSomething(Foo x @expand(x.val));
extern fn void fooSomething(Foo x.val);
extern fn void fooSomething(Foo x @expand(val);
extern fn void fooSomething(Foo x -> x.val);
extern fn void fooSomething(Foo x @extern(x.val));
extern fn void fooSomething(Foo x @export(x.val));

Of course there is the explicit macro wrapper too.

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

There are probably other alternatives.

@OdnetninI
Copy link
Contributor

A new type seems overkill.

As this is something intended for C interop, @expand(x.val) seems appropiate.
But other options may be better.

Also, Foo.val seems good, but with that kind of syntax, I am worried people start using it outside C interfaces...

@lerno
Copy link
Collaborator Author

lerno commented Aug 6, 2024

If it's used outside of C interfaces, that's probably bad, unless there is a good reason for it.

@Hema2-official
Copy link
Contributor

Hema2-official commented Aug 22, 2024

If I understand correctly, the current implementation uses the type of the enum to store an identifier, and then it expands a virtual struct with the associated values. So this:

enum Channels : char (char size)
{
    AUTO = 0,
    RGB = 3,
    RGBA = 4
}

...acts similar (memory-wise) to something like this:

struct Channel @packed
{
    char id;    // identifier
    char size;  // associated value
}

So to set values to enum members, the requirements would be:

  • be the same type
  • be the same size

So why not just make an attribute that simply makes an enum act like the ones in C?
And the attribute would require all members to have unique values. For example:

enum Channels : char @unique
{
    AUTO = 0,
    RGB = 3,
    YUV = 3
}

...would fail, since Channels.RGB and Channels.YUV have the same value.
And for a less evil example:

enum Channels : char @unique
{
    AUTO = 0,
    RGB = 3,
    RGBA = 4
}

This would compile successfully, have the size of a char, and implicitly or explicitly convert to it. It would also be possible to then have a macro to convert a char value to an optional Channels. For example:

Channels! some_result = enumcast(6, Channels); // is a fault
Channels! other_result = enumcast(3, Channels); // is Channels.RGB

Tell me what you guys think.
If this all sounds good, I'll lead the implementation if needed.

peace<3

@lerno
Copy link
Collaborator Author

lerno commented Aug 22, 2024

Something like enum Channels : char (char size) is represented by a char. That is also its size. To get the size value, there is a (hidden) global array containing the values, so:

Channels x;
char y = x.size;

Is really:

char x;
char y = __Channels_size[x];

So then you understand that given that you have the size, to find the Channels entry, one needs to iterate over all entries in the __Channels_size array until a match is found. This can be trivially implemented as a macro, but it should not be a built-in feature as:

  1. Lookup cost is proportional to the number of elements in the array
  2. There are possible ambiguities that can only be resolved by selecting the first match
  3. As associated values may be any type, equals may not even be defined, or may be arbitrarily complex.

@Hema2-official
Copy link
Contributor

Hema2-official commented Aug 22, 2024

Thanks for the explanation!

And I agree with you on your takes, they're kinda obvious. But what your arguments are against is not what I wanted to describe.

With @unique (or whatever), associated values wouldn't be possible. It would look like something along these lines:

enum Channels : char @unique
{
    AUTO = 0,
    RGB = 3,
    RGBA = 4
}

// =>

// effectively (when using the value):
distinct Channels = char; // 🤷‍♂️

In memory, we'd have to retain a value for every entry up until 4. So in C++ terms:

const int CHANNELS_COUNT = 5;

bool Channels[] = {
    true,
    false,
    false,
    true,
    true
};

bool isPresent(char index) {
    return (index < CHANNELS_COUNT) && Channels[index];
}

int main() {
    isPresent(0); // true
    isPresent(2); // false
    isPresent(4); // true
    isPresent(5); // false
    // etc...

    return 0;
}

And to be less of a memory hog:

const int CHANNELS_COUNT = 5;

long Channels = 0b10011;

bool isPresent(char index) {
    return (index < CHANNELS_COUNT) && (Channels & (1 >> index));
}

// same tests and results as before ...

Or if we want to retain identifier strings:

const int CHANNELS_COUNT = 5;

void* Channels[] = {
    (void*)"AUTO",
    NULL,
    NULL,
    (void*)"RGB",
    (void*)"RGBA"
};

bool isPresent(char index) {
    return (index < CHANNELS_COUNT) && Channels[index];
}

int main() {
    char index = 4;
    bool result = isPresent(index);
    if (result == true)
        cout << (char*)Channels[index];
    else
        cout << "That's a miss";

    return 0;
}

Same thing as the first but now, a NULL marks a fault for e.g. NONEXISTENT_ENUM_MEMBER and otherwise we can do .nameof or something...

This way, the performance hit is kept pretty low, especially for desktop systems. Still, 2 or 3 times C, but eh.
Sadly, the 3rd example hasn't really heard of memory efficiency, and for OpenGL it would be about 1MB for just the lookup table, but this is ONLY if we want to retain identifiers. Otherwise, it's about 30KB.

So this solution:

  1. Discards associated values (unfortunately), but
  2. Enables optional reverse lookup without a significant performance hit
  3. Enables simple usage of the values, like in C

Thoughts?
(Also, I'm starting to feel the complexity of this question and why most ideas are discarded...)

@lerno
Copy link
Collaborator Author

lerno commented Aug 22, 2024

Did you think about the problem here that C enums also cover constants-that-are-bitmasks, and so to handle this yet another annotation is needed.

@Hema2-official
Copy link
Contributor

Well, they would work, as the only requirement for my sketch is for the values to be unique, but yeah, it would be pretty inefficient...
So yeah, no reverse lookup.

A new type seemed kinda overkill to me as well.
Is there maybe another idea on how to bind constants to a parent? Because that's what the new type is about, isn't it?
Some kinda static struct, perhaps?

(Sorry for the broken terminology btw.)

@lerno
Copy link
Collaborator Author

lerno commented Aug 23, 2024

There is the sub-module approach:

module foo_bindings;
/* function and types here */
module foo_bindings::channels;

distinct Channels = int;
const Channels AUTO = 0;
const Channels RGB = 3;
const Channels RGBA = 4;

Now we can use it in this manner:

fn void main()
{
   Channels channel = channels::RGB;
   int a = 1 + (int)channel;
   Channels rgba = (Channels)a;
   assert(rgba == channels::RGBA)
}

Here the module name becomes the effective namespace.

@TheOnlySilverClaw
Copy link

TheOnlySilverClaw commented Sep 7, 2024

If I may chime in here. I'm currently building a few C bindings with extensive use of defined values. And for that, I actually have several use cases for enums as a namespace with a mix of manually defined and auto-incrementing values.

Just one example from GLFW that would be wonderful if it worked like this:

enum KeyCode : CInt {
  UNKNOWN = -1,
  SPACE = 32,
  APOSTROPHE = 39,
  COMMA = 44,
  MINUS, // auto-incremented to 45
  PERIOD,  // auto-incremented to 46
  ...
}

The submodule approach is a usable workaround, but ends up being quite inconsistent when working with some values that fit an enum and others that don't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion needed This feature needs discussion to iron out details Enhancement Request New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants