Generic dynamic array of enums

So I want a dynamic array of a generic enum type. Something like this:

package enum_test

import "base:intrinsics"

foo :: struct ( $E : typeid ) where intrinsics.type_is_enum ( E ) {
    id: E
}

bar :: struct { into : []dynamic foo }

blarg :: enum { a, b }

point :: enum { zort, narf }

main :: proc() {
    bleh : bar
    append( &bleh.into, foo( blarg ) { blarg.a } )

    blue : bar
    append( &blue.into, foo( point ) { point.zort } )
}

The problem is that I want an enum inside a parapoly struct , then to put that struct inside a monomorphic struct. Enumerated arrays won’t work, they are an array with size = enum which is a different concept.

I understand that if enum types are solved at compile-time, then the fields in a struct need to know the size of the type, and enums can be an integer of any size. If I could use a generic enum type where I specify the integer type (int, u8, … ) this problem would be solved, and as far as I know there’s no such thing.

Having something like a generic enum type would be difficult since an enum is not a specific type, but a group of types. That’s why ‘enum’ is a keyword. Even if I didn’t want an array but a single ‘generic’ enum type without specifying the specific type of enum, there’s no generic enum.

Looking at the overview, I’d say that something like ‘enum integer’ (or integer enum) is an abstract type, though I feel the untyped types in the overview can only be constants, which is not what I want. An ‘enum integer’ would require moving the concept of enum from keyword to built-in type.

I ranted this much because I was trying to see if I am missing something.

The one way I have thought of to solve this is to store the enum value as an int, and the enum type in a type id. When I want to use the enum, I’d cast the int to the enum type during runtime. I’d prefer if it were solved during compilation.

A c-style enum would allow at least using the enum element identifiers as indexes, even if I losethe stricter typing. I could achieve something similar by making a bunch of constants, but then I lose the syntactic sugar of enums.

If anyone has any other ideas on how to solve this, let me know.

Ultimately, it’s up to you to somehow know which element is which type, so:

  • you could either store a typeid per element,
  • if you know which element is which type in some other way, you could do the type erasure way where all the enums have the same backing integer,
  • or perhaps if those dynamic arrays are all the same enum you could cast them (but it doesn’t sound like that).

It would help to know more about the actual use case, because I’m just throwing some general possibilities around.

1 Like

So I thought about this a lot, and I think I’m just asking for the impossible. You would be able to covert to an abstract type at compilation time but no matter what it’s not possible to do this in this manner without runtime polymorphism.

I could have also done during compilation time if struct constants from parapoly expressions were a thing.

Welp, I found that there are compile-time constants in structs / unions, they just not mentioned as such :sweat_smile:

So after all I just need to get a proper mental model of Odin instead of ranting and letting my mind overcomplicate things :upside_down_face:

Thanks for the reply though! It’s appreciated.

The following code is probably not the solution you’re looking for.
However, it provides a semi-generic enum type, if that’s acceptable.

I'm sharing it in case others find it useful.
package main

Room :: enum {A, B, C}
Level :: enum {D, E, F}

Gen_Enum :: union #no_nil {
	Room,
	Level,
}

Foo :: struct {id: Gen_Enum}
Bar :: struct {into: [dynamic]Foo}

main :: proc() {
    fst: Bar
    append(&fst.into, Foo{Room.A})

    snd: Bar
    append(&snd.into, Foo{Level.D})
}

That is not what I was looking for, but since I’m just a beginner with Odin, anything I learn is welcome, for example, I didn’t know about #no_nil and I didn’t think about using unions that way either.

Here's how I solved it
package enum_test

import "base:intrinsics"
import "core:fmt"

_Group :: struct( $T : typeid ) where intrinsics.type_is_enum( T ) { // internal use, not API
    id : T,
    name : string,
    deprecated : bool
}

Singleton :: struct( $T : typeid ) // T serves as compile-time storage
    where intrinsics.type_is_enum( T ) {

    groups : [ dynamic ]_Group( T )
}

add :: proc( singleton : ^Singleton( $T ), id : T, name : string, deprecated : bool = false ) {
    group : _Group( T ) = { id, name, deprecated }
    append( &singleton.groups, group )
}

evaluate :: proc( singleton : Singleton( $T ), text : string ) -> T {
    for group in singleton.groups {
        if group.name == text {

            if group.deprecated {
                fmt.printf( "%v has been deprecated, please use an alternative\n", group.name )
            }

            return group.id
        }
    }
    
    NONE_VALUE :: 0
    return transmute( T ) int( NONE_VALUE )
} 

ImportantEnum :: enum { // User created
     NONE, // 0 is reserved, must exist and be none
     GROUP_A_NAME1,
     GROUP_A_NAME2,
     GROUP_B_NAME1,
}

main :: proc() {
    singleton : Singleton( ImportantEnum )
    add( &singleton, ImportantEnum.GROUP_A_NAME1, "example text" )
    fmt.printf( "%v\n", evaluate( singleton, "example text" ))
}


I want to make the interface as easy and as fast as it would be in C, and I think this achieves it. Without spending time on that embarrassingly overcomplicated mental rabbit hole I would not have figured it out. I also had to learn that Odin has no dynamic casts through testing, and since the cast and transmute operators are evaluated during compilation, there’s no loss of runtime performance.

The magic is that thanks to intrinsics parapoly args can be generic enums, and parapoly args are effectively struct compile-time constant fields, and thanks to intrinsics I got the generic enum I wanted:

Singleton :: struct( $T : typeid ) where intrinsics.type_is_enum( T )

Parapoly args can even be used in functions to convert an int to enums:

    NONE_VALUE :: 0
    return transmute( T ) int( NONE_VALUE )
1 Like

Just skip the whole parapoly struct part, there is runtime type information where you can get back the details of what the enum was.

package any_enum

import "base:intrinsics"
import "base:runtime"
import "core:fmt"

_Enum :: [size_of(enum {})]byte

Any_Enum :: struct {
	data : _Enum,
	id   : typeid,
}

any_enum :: proc (value: $E) -> (ae: Any_Enum)
	where intrinsics.type_is_enum(E),
	      size_of(E) <= size_of(_Enum)
{
	bs := (transmute([size_of(E)]byte)value)
	ae.id = E
	copy(ae.data[:], bs[:])
	return ae
}

to_builtin_any :: proc (ae: ^Any_Enum) -> any {
	return transmute(any)runtime.Raw_Any {
		id   = ae.id,
		data = rawptr(&ae.data[0])
	}
}

A :: enum {
	A0,
	A1,
	A2,
	A3,
}

B :: enum i16 {
	B0,
	B1,
	B2,
	B3,
}

Enums :: [dynamic]Any_Enum

main :: proc () {
	es := make(Enums)
	defer delete(es)

	for a in A do append(&es, any_enum(a))
	for b in B do append(&es, any_enum(b))

	for &e in es {
		fmt.println(to_builtin_any(&e))
	}
}
1 Like