Skip to content

Commit

Permalink
Allow explicit enum values (#114)
Browse files Browse the repository at this point in the history
* allow setting explicit values for enum

* add test for explicit enum

* format

* change syntax for explicit enum values

* format

* add doc for enum_value

* handle explicit enums in generator

* fix explicit enum tests

* update nif text fixtures for explicit enums

* add info about atoms for explicit enums
  • Loading branch information
harrisi authored Nov 13, 2024
1 parent 204045b commit 84c563b
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 6 deletions.
11 changes: 7 additions & 4 deletions lib/unifex/code_generator/base_types/enum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Unifex.CodeGenerator.BaseTypes.Enum do

@impl true
def generate_native_type(ctx) do
~g<#{ctx.type_spec.name |> Atom.to_string() |> Macro.camelize()}>
~g<#{ctx.type_spec.name |> enum_to_string() |> Macro.camelize()}>
end

defmodule NIF do
Expand Down Expand Up @@ -114,7 +114,7 @@ defmodule Unifex.CodeGenerator.BaseTypes.Enum do
enum_name = ctx.type_spec.name

ctx.type_spec.types
|> Enum.map(&Atom.to_string/1)
|> Enum.map(&enum_to_string/1)
|> Enum.map_join(" else ", fn type ->
~g"""
if (strcmp(enum_as_string, "#{type}") == 0) {
Expand All @@ -134,11 +134,11 @@ defmodule Unifex.CodeGenerator.BaseTypes.Enum do
CodeGenerator.code_t()
def do_generate_arg_serialize_if_statements(name, ctx, serializer) do
{last_type, types} = List.pop_at(ctx.type_spec.types, -1)
last_type = Atom.to_string(last_type)
last_type = enum_to_string(last_type)
enum_name = ctx.type_spec.name

types
|> Enum.map(&Atom.to_string/1)
|> Enum.map(&enum_to_string/1)
|> Enum.map(fn type ->
~g"""
if (#{name} == #{String.upcase("#{enum_name}_#{type}")}) {
Expand All @@ -149,4 +149,7 @@ defmodule Unifex.CodeGenerator.BaseTypes.Enum do
|> Enum.concat(["{ #{serializer.(last_type, ctx)} }"])
|> Enum.join(" else ")
end

defp enum_to_string(name) when is_atom(name), do: Atom.to_string(name)
defp enum_to_string({name, _val}) when is_atom(name), do: Atom.to_string(name)
end
5 changes: 4 additions & 1 deletion lib/unifex/code_generators/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ defmodule Unifex.CodeGenerators.Common do
@spec generate_enum_native_definition(Specs.enum_t(), map) :: CodeGenerator.code_t()
def generate_enum_native_definition({enum_name, enum_types}, _ctx) do
enum_types =
Enum.map_join(enum_types, ",\n", fn type -> String.upcase("#{enum_name}_#{type}") end)
Enum.map_join(enum_types, ",\n", fn
{type, val} -> String.upcase("#{enum_name}_#{type} = #{val}")
type -> String.upcase("#{enum_name}_#{type}")
end)

enum_name =
enum_name
Expand Down
12 changes: 12 additions & 0 deletions lib/unifex/specs_dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ defmodule Unifex.Specs.DSL do
type my_enum :: :option_one | :option_two | :option_three | ...
Enum constants can be given an explicit value with `enum_value`
type my_explicit_enum :: enum_value(:option_one, 1) | :option_two | :option_three | ...
The numeric value assigned to any enum constant can only be used from C/C++.
In Elixir, the atom must be used.
Struct or enums specified in such way can be used in like any other supported type, E.g.
spec my_function(in_enum :: my_enum) :: {:ok :: label, out_struct :: my_struct}
Expand Down Expand Up @@ -265,6 +272,11 @@ defmodule Unifex.Specs.DSL do
[type_name]
end

defp parse_enum_types({:enum_value, _meta, [type_name, value]})
when is_atom(type_name) and is_integer(value) do
[{type_name, value}]
end

defp parse_enum_types({:|, _meta, [first_arg, second_arg]}) do
parse_enum_types(first_arg) ++ parse_enum_types(second_arg)
end
Expand Down
75 changes: 75 additions & 0 deletions test/fixtures/nif_ref_generated/nif/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,35 @@ UNIFEX_TERM test_my_enum_result_ok(UnifexEnv *env, MyEnum out_enum) {
});
}

UNIFEX_TERM test_my_explicit_enum_result_ok(UnifexEnv *env,
MyExplicitEnum out_enum) {
return ({
const ERL_NIF_TERM terms[] = {enif_make_atom(env, "ok"), ({
ERL_NIF_TERM res;
if (out_enum == MY_EXPLICIT_ENUM_A) {
const char *enum_as_string = "a";
res = enif_make_atom(env, enum_as_string);

} else if (out_enum == MY_EXPLICIT_ENUM_B) {
const char *enum_as_string = "b";
res = enif_make_atom(env, enum_as_string);

} else if (out_enum == MY_EXPLICIT_ENUM_C) {
const char *enum_as_string = "c";
res = enif_make_atom(env, enum_as_string);

} else {
const char *enum_as_string = "d";
res = enif_make_atom(env, enum_as_string);
}
res;
})

};
enif_make_tuple_from_array(env, terms, 2);
});
}

UNIFEX_TERM test_nil_bugged_result_nil(UnifexEnv *env) {
return enif_make_atom(env, "nil");
}
Expand Down Expand Up @@ -1253,6 +1282,51 @@ static ERL_NIF_TERM export_test_my_enum(ErlNifEnv *env, int argc,
return result;
}

static ERL_NIF_TERM export_test_my_explicit_enum(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
UNIFEX_MAYBE_UNUSED(argc);
UNIFEX_MAYBE_UNUSED(argv);
ERL_NIF_TERM result;
UnifexEnv *unifex_env = env;
MyExplicitEnum in_enum;

if (!({
int res = 0;
char *enum_as_string = NULL;

if (unifex_alloc_and_get_atom(env, argv[0], &enum_as_string)) {
if (strcmp(enum_as_string, "a") == 0) {
in_enum = MY_EXPLICIT_ENUM_A;
res = 1;
} else if (strcmp(enum_as_string, "b") == 0) {
in_enum = MY_EXPLICIT_ENUM_B;
res = 1;
} else if (strcmp(enum_as_string, "c") == 0) {
in_enum = MY_EXPLICIT_ENUM_C;
res = 1;
} else if (strcmp(enum_as_string, "d") == 0) {
in_enum = MY_EXPLICIT_ENUM_D;
res = 1;
}

if (enum_as_string != NULL) {
unifex_free((void *)enum_as_string);
}
}

res;
})) {
result = unifex_raise_args_error(env, "in_enum", ":my_explicit_enum");
goto exit_export_test_my_explicit_enum;
}

result = test_my_explicit_enum(unifex_env, in_enum);
goto exit_export_test_my_explicit_enum;
exit_export_test_my_explicit_enum:

return result;
}

static ERL_NIF_TERM export_test_nil_bugged(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
UNIFEX_MAYBE_UNUSED(argc);
Expand Down Expand Up @@ -1305,6 +1379,7 @@ static ErlNifFunc nif_funcs[] = {
{"unifex_test_nested_struct", 1, export_test_nested_struct, 0},
{"unifex_test_list_of_structs", 1, export_test_list_of_structs, 0},
{"unifex_test_my_enum", 1, export_test_my_enum, 0},
{"unifex_test_my_explicit_enum", 1, export_test_my_explicit_enum, 0},
{"unifex_test_nil_bugged", 0, export_test_nil_bugged, 0},
{"unifex_test_nil_tuple_bugged", 1, export_test_nil_tuple_bugged, 0}};

Expand Down
75 changes: 75 additions & 0 deletions test/fixtures/nif_ref_generated/nif/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,35 @@ UNIFEX_TERM test_my_enum_result_ok(UnifexEnv *env, MyEnum out_enum) {
});
}

UNIFEX_TERM test_my_explicit_enum_result_ok(UnifexEnv *env,
MyExplicitEnum out_enum) {
return ({
const ERL_NIF_TERM terms[] = {enif_make_atom(env, "ok"), ({
ERL_NIF_TERM res;
if (out_enum == MY_EXPLICIT_ENUM_A) {
const char *enum_as_string = "a";
res = enif_make_atom(env, enum_as_string);

} else if (out_enum == MY_EXPLICIT_ENUM_B) {
const char *enum_as_string = "b";
res = enif_make_atom(env, enum_as_string);

} else if (out_enum == MY_EXPLICIT_ENUM_C) {
const char *enum_as_string = "c";
res = enif_make_atom(env, enum_as_string);

} else {
const char *enum_as_string = "d";
res = enif_make_atom(env, enum_as_string);
}
res;
})

};
enif_make_tuple_from_array(env, terms, 2);
});
}

UNIFEX_TERM test_nil_bugged_result_nil(UnifexEnv *env) {
return enif_make_atom(env, "nil");
}
Expand Down Expand Up @@ -1253,6 +1282,51 @@ static ERL_NIF_TERM export_test_my_enum(ErlNifEnv *env, int argc,
return result;
}

static ERL_NIF_TERM export_test_my_explicit_enum(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
UNIFEX_MAYBE_UNUSED(argc);
UNIFEX_MAYBE_UNUSED(argv);
ERL_NIF_TERM result;
UnifexEnv *unifex_env = env;
MyExplicitEnum in_enum;

if (!({
int res = 0;
char *enum_as_string = NULL;

if (unifex_alloc_and_get_atom(env, argv[0], &enum_as_string)) {
if (strcmp(enum_as_string, "a") == 0) {
in_enum = MY_EXPLICIT_ENUM_A;
res = 1;
} else if (strcmp(enum_as_string, "b") == 0) {
in_enum = MY_EXPLICIT_ENUM_B;
res = 1;
} else if (strcmp(enum_as_string, "c") == 0) {
in_enum = MY_EXPLICIT_ENUM_C;
res = 1;
} else if (strcmp(enum_as_string, "d") == 0) {
in_enum = MY_EXPLICIT_ENUM_D;
res = 1;
}

if (enum_as_string != NULL) {
unifex_free((void *)enum_as_string);
}
}

res;
})) {
result = unifex_raise_args_error(env, "in_enum", ":my_explicit_enum");
goto exit_export_test_my_explicit_enum;
}

result = test_my_explicit_enum(unifex_env, in_enum);
goto exit_export_test_my_explicit_enum;
exit_export_test_my_explicit_enum:

return result;
}

static ERL_NIF_TERM export_test_nil_bugged(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
UNIFEX_MAYBE_UNUSED(argc);
Expand Down Expand Up @@ -1305,6 +1379,7 @@ static ErlNifFunc nif_funcs[] = {
{"unifex_test_nested_struct", 1, export_test_nested_struct, 0},
{"unifex_test_list_of_structs", 1, export_test_list_of_structs, 0},
{"unifex_test_my_enum", 1, export_test_my_enum, 0},
{"unifex_test_my_explicit_enum", 1, export_test_my_explicit_enum, 0},
{"unifex_test_nil_bugged", 0, export_test_nil_bugged, 0},
{"unifex_test_nil_tuple_bugged", 1, export_test_nil_tuple_bugged, 0}};

Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/nif_ref_generated/nif/example.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ enum MyEnum_t {
typedef enum MyEnum_t MyEnum;
#endif

#ifdef __cplusplus
enum MyExplicitEnum {
MY_EXPLICIT_ENUM_A = 1,
MY_EXPLICIT_ENUM_B,
MY_EXPLICIT_ENUM_C = 4,
MY_EXPLICIT_ENUM_D = 8
};
#else
enum MyExplicitEnum_t {
MY_EXPLICIT_ENUM_A = 1,
MY_EXPLICIT_ENUM_B,
MY_EXPLICIT_ENUM_C = 4,
MY_EXPLICIT_ENUM_D = 8
};
typedef enum MyExplicitEnum_t MyExplicitEnum;
#endif

#ifdef __cplusplus
struct my_struct {
int id;
Expand Down Expand Up @@ -148,6 +165,7 @@ UNIFEX_TERM test_nested_struct(UnifexEnv *env, nested_struct in_struct);
UNIFEX_TERM test_list_of_structs(UnifexEnv *env, simple_struct *struct_list,
unsigned int struct_list_length);
UNIFEX_TERM test_my_enum(UnifexEnv *env, MyEnum in_enum);
UNIFEX_TERM test_my_explicit_enum(UnifexEnv *env, MyExplicitEnum in_enum);
UNIFEX_TERM test_nil_bugged(UnifexEnv *env);
UNIFEX_TERM test_nil_tuple_bugged(UnifexEnv *env, int in_int);

Expand Down Expand Up @@ -188,6 +206,8 @@ UNIFEX_TERM test_list_of_structs_result_ok(UnifexEnv *env,
simple_struct const *out_struct_list,
unsigned int out_struct_list_length);
UNIFEX_TERM test_my_enum_result_ok(UnifexEnv *env, MyEnum out_enum);
UNIFEX_TERM test_my_explicit_enum_result_ok(UnifexEnv *env,
MyExplicitEnum out_enum);
UNIFEX_TERM test_nil_bugged_result_nil(UnifexEnv *env);
UNIFEX_TERM test_nil_tuple_bugged_result_nil(UnifexEnv *env, int out_int);

Expand Down
4 changes: 4 additions & 0 deletions test_projects/nif/c_src/example/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ UNIFEX_TERM test_my_enum(UnifexEnv *env, MyEnum in_enum) {
return test_my_enum_result_ok(env, in_enum);
}

UNIFEX_TERM test_my_explicit_enum(UnifexEnv *env, MyExplicitEnum in_enum) {
return test_my_explicit_enum_result_ok(env, in_enum);
}

UNIFEX_TERM test_nested_struct(UnifexEnv *env, nested_struct in_struct) {
return test_nested_struct_result_ok(env, in_struct);
}
Expand Down
5 changes: 5 additions & 0 deletions test_projects/nif/c_src/example/example.spec.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,16 @@ spec test_list_of_structs(struct_list :: [simple_struct]) :: {:ok :: label, out_

type my_enum :: :option_one | :option_two | :option_three | :option_four | :option_five

type my_explicit_enum :: enum_value(:a, 1) | :b | enum_value(:c, 4) | enum_value(:d, 8)

@doc """
test_my_enum docs
"""
spec test_my_enum(in_enum :: my_enum) :: {:ok :: label, out_enum :: my_enum}


spec test_my_explicit_enum(in_enum :: my_explicit_enum) :: {:ok :: label, out_enum :: my_explicit_enum}

# tests for bugged version of functions returning nil.
# these tests should be removed in unifex v2.0.0. For more information check:
# https://github.com/membraneframework/membrane_core/issues/758
Expand Down
13 changes: 12 additions & 1 deletion test_projects/nif/test/example_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,23 @@ defmodule ExampleTest do
end
end

test "explicit enum" do
assert {:ok, :a} = Example.test_my_explicit_enum(:a)
assert {:ok, :b} = Example.test_my_explicit_enum(:b)
assert {:ok, :c} = Example.test_my_explicit_enum(:c)
assert {:ok, :d} = Example.test_my_explicit_enum(:d)

assert_raise ErlangError, ~r/unifex_parse_arg.*in_enum.*my_explicit_enum/i, fn ->
Example.test_my_explicit_enum(:option_not_mentioned)
end
end

test "nested struct list" do
my_struct = %My.Struct{id: 1, name: "Jan Kowlaski", data: [1, 2, 3, 4, 5, 6, 7, 8, 9]}
nested_struct_list = %Nested.StructList{id: 1, struct_list: [my_struct]}
assert {:ok, ^nested_struct_list} = Example.test_nested_struct_list(nested_struct_list)
end

# tests for bugged version of functions returning nil.
# these tests should be removed in unifex v2.0.0. For more information check:
# https://github.com/membraneframework/membrane_core/issues/758
Expand Down

0 comments on commit 84c563b

Please sign in to comment.