From 976c0c02411dec6771f41c809fce06e272624000 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 16 Jun 2026 17:51:32 +0900 Subject: [PATCH 1/4] Add List.to_existing_atom/2 --- lib/elixir/lib/list.ex | 42 ++++++++++++++++++++++++++++ lib/elixir/test/elixir/list_test.exs | 19 +++++++++++++ 2 files changed, 61 insertions(+) diff --git a/lib/elixir/lib/list.ex b/lib/elixir/lib/list.ex index b6c3042e6f6..ec7d03921d7 100644 --- a/lib/elixir/lib/list.ex +++ b/lib/elixir/lib/list.ex @@ -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. @@ -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(atom)) :: 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`. diff --git a/lib/elixir/test/elixir/list_test.exs b/lib/elixir/test/elixir/list_test.exs index 9567b372acf..a34f91bdf52 100644 --- a/lib/elixir/test/elixir/list_test.exs +++ b/lib/elixir/test/elixir/list_test.exs @@ -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 From 783686766480398ac10bf0d13d1a758a32db9501 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 16 Jun 2026 17:54:11 +0900 Subject: [PATCH 2/4] Make typespec parametric --- lib/elixir/lib/list.ex | 2 +- lib/elixir/lib/string.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/list.ex b/lib/elixir/lib/list.ex index ec7d03921d7..043aecd67ff 100644 --- a/lib/elixir/lib/list.ex +++ b/lib/elixir/lib/list.ex @@ -1099,7 +1099,7 @@ defmodule List do ** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar] """ - @spec to_existing_atom(charlist, nonempty_list(atom)) :: atom + @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) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index d6cd7baa109..80a8b87f2ab 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -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) From dde8ae926548869961721af3934e48d6e75d2534 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 16 Jun 2026 18:03:39 +0900 Subject: [PATCH 3/4] Optimize List.to_existing_atom in the compiler --- lib/elixir/src/elixir_erl_pass.erl | 12 ++++++++---- lib/elixir/test/erlang/control_test.erl | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index 3674bd2a763..7153ce01a57 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -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), @@ -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), diff --git a/lib/elixir/test/erlang/control_test.erl b/lib/elixir/test/erlang/control_test.erl index 597d6a933f8..2d99f87b0ca 100644 --- a/lib/elixir/test/erlang/control_test.erl +++ b/lib/elixir/test/erlang/control_test.erl @@ -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}}, From 5615b11ae0e0850c5f78041aa8b9cf2ba770581d Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 16 Jun 2026 18:49:04 +0900 Subject: [PATCH 4/4] Refine return type --- lib/elixir/lib/module/types/apply.ex | 6 ++-- .../test/elixir/module/types/expr_test.exs | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index cb28ae89916..9066cfb2e9e 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -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() @@ -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() diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index e37befa9d8e..25134a7bc11 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -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