Skip to content
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
42 changes: 42 additions & 0 deletions lib/elixir/lib/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,8 @@ defmodule List do
@doc """
Converts a charlist to an existing atom.

If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.

Elixir supports conversions from charlists which contain any Unicode
code point. Raises an `ArgumentError` if the atom does not exist.

Expand Down Expand Up @@ -1079,6 +1081,46 @@ defmodule List do
:erlang.list_to_existing_atom(charlist)
end

@doc """
Converts a charlist to one of the `allowed_atoms` or raises.

Raises an `ArgumentError` if the atom either does not exist or is not within
the existing list.

This should be preferred to `to_existing_atom/1` if the list is known upfront,
since there is no risk that the atom has not been loaded.

## Examples

iex> List.to_existing_atom(~c"foo", [:foo, :bar])
:foo

iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]

"""
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
atom = :erlang.list_to_existing_atom(charlist)

if atom not in allowed_atoms do
to_existing_atom_unexpected(charlist, allowed_atoms)
end

atom
end

# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(charlist, allowed_atoms) do
to_existing_atom_unexpected(charlist, allowed_atoms)
end

defp to_existing_atom_unexpected(charlist, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end

@doc """
Returns the float whose text representation is `charlist`.

Expand Down
6 changes: 4 additions & 2 deletions lib/elixir/lib/module/types/apply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ defmodule Module.Types.Apply do
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
{:maps, :values, [{[open_map()], list(term())}]},
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]}
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]},
{List, :to_existing_atom, [{[list(integer()), non_empty_list(atom())], atom()}]}
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()

Expand Down Expand Up @@ -1419,7 +1420,8 @@ defmodule Module.Types.Apply do
end
end

defp remote_apply(String, :to_existing_atom, info, [_string, list] = args_types, stack) do
defp remote_apply(mod, :to_existing_atom, info, [_string, list] = args_types, stack)
when mod in [String, List] do
# TODO remove once we add parametric types, this will just be:
# binary(), non_empty_list(a) -> a when a: atom()

Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/lib/string.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3041,7 +3041,7 @@ defmodule String do
** (ArgumentError) unexpected value: \"unknown\", the allowed atoms are: [:foo, :bar]

"""
@spec to_existing_atom(String.t(), nonempty_list(atom)) :: atom
@spec to_existing_atom(String.t(), nonempty_list(a)) :: a when a: atom()
def to_existing_atom(string, [_ | _] = allowed_atoms) when is_binary(string) do
atom = :erlang.binary_to_existing_atom(string, :utf8)

Expand Down
12 changes: 8 additions & 4 deletions lib/elixir/src/elixir_erl_pass.erl
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ translate_remote(maps, merge, Meta, [Map1, Map2], S) ->
{[TMap1, TMap2], TS} ->
{{call, Ann, {remote, Ann, {atom, Ann, maps}, {atom, Ann, merge}}, [TMap1, TMap2]}, TS}
end;
translate_remote('Elixir.String', to_existing_atom, Meta, [String, List], S) ->
translate_remote(Mod, to_existing_atom, Meta, [String, List], S) when Mod == 'Elixir.String'; Mod == 'Elixir.List' ->
Ann = ?ann(Meta),
{[TString, TList], TS} = translate_args([String, List], Ann, S),

Expand All @@ -644,16 +644,20 @@ translate_remote('Elixir.String', to_existing_atom, Meta, [String, List], S) ->
LastClause = {clause, Generated,
[{var, Generated, '_'}],
[],
[{call, Ann, {remote, Ann, {atom, Ann, 'Elixir.String'}, {atom, Ann, '__to_existing_atom__'}}, [TString, TList]}]},
[{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, '__to_existing_atom__'}}, [TString, TList]}]},
CastAtom = case Mod of
'Elixir.String' -> fun atom_to_binary/1;
'Elixir.List' -> fun atom_to_list/1
end,
Clauses = [
{clause, Generated,
[{bin, Generated, [{bin_element, Generated, {string, Generated, atom_to_list(Atom)}, default, default}]}],
[elixir_erl:elixir_to_erl(CastAtom(Atom), Ann)],
[],
[{atom, Ann, Atom}]}
|| Atom <- List] ++ [LastClause],
{{'case', Generated, TString, Clauses}, TS};
false ->
{{call, Ann, {remote, Ann, {atom, Ann, 'Elixir.String'}, {atom, Ann, to_existing_atom}}, [TString, TList]}, TS}
{{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, to_existing_atom}}, [TString, TList]}, TS}
end;
translate_remote(Left, Right, Meta, Args, S) ->
Ann = ?ann(Meta),
Expand Down
19 changes: 19 additions & 0 deletions lib/elixir/test/elixir/list_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,23 @@ defmodule ListTest do
assert List.ascii_printable?(~c"abc" ++ ?d, 3)
end
end

test "to_existing_atom/2" do
# constant
assert List.to_existing_atom(~c"foo", [:foo, :bar]) == :foo
assert List.to_existing_atom(~c"bar", [:foo, :bar]) == :bar

assert_raise ArgumentError, fn ->
List.to_existing_atom(~c"baz", [:foo, :bar])
end

# variable
values = [:foo, :bar]
assert List.to_existing_atom(~c"foo", values) == :foo
assert List.to_existing_atom(~c"bar", values) == :bar

assert_raise ArgumentError, fn ->
List.to_existing_atom(~c"baz", values)
end
end
end
35 changes: 35 additions & 0 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,41 @@ defmodule Module.Types.ExprTest do
String.to_existing_atom(x, [])
) =~ "incompatible types given to String.to_existing_atom/2"
end

test "List.to_existing_atom/2" do
assert typecheck!(
[x],
List.to_existing_atom(x, [:foo, :bar])
) == atom([:foo, :bar])

assert typecheck!(
[x],
(
values = [:foo, :bar]
List.to_existing_atom(x, values)
)
) == atom([:foo, :bar])

assert typecheck!(
[x, values],
List.to_existing_atom(x, values)
) == dynamic(atom())

assert typeerror!(
[x],
List.to_existing_atom(:not_a_charlist, x)
) =~ "incompatible types given to List.to_existing_atom/2"

assert typeerror!(
[x],
List.to_existing_atom(x, [:foo, "not atoms"])
) =~ "incompatible types given to List.to_existing_atom/2"

assert typeerror!(
[x],
List.to_existing_atom(x, [])
) =~ "incompatible types given to List.to_existing_atom/2"
end
end

describe "case" do
Expand Down
16 changes: 16 additions & 0 deletions lib/elixir/test/erlang/control_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ optimized_string_to_existing_atom_test() ->
[{call, _, {remote, _, {atom, _, 'Elixir.String'}, {atom, _, '__to_existing_atom__'}}, [_, _]}]}]
} = to_erl("String.to_existing_atom(\"baz\", [:foo, :bar])").

optimized_list_to_existing_atom_test() ->
{'case', _, _,
[{clause, _,
[{cons, _, {integer, _, $f}, {cons, _, {integer, _, $o}, {cons, _, {integer, _, $o}, {nil, _}}}}],
[],
[{atom, _, foo}]},
{clause, _,
[{cons, _, {integer, _, $b}, {cons, _, {integer, _, $a}, {cons, _, {integer, _, $r}, {nil, _}}}}],
[],
[{atom, _, bar}]},
{clause, _,
[{var, _, '_'}],
[],
[{call, _, {remote, _, {atom, _, 'Elixir.List'}, {atom, _, '__to_existing_atom__'}}, [_, _]}]}]
} = to_erl("List.to_existing_atom(~c\"baz\", [:foo, :bar])").

optimized_map_merge_test() ->
{map, _,
[{map_field_assoc, _, {atom, _, a}, {integer, _, 1}},
Expand Down
Loading