Skip to content

Commit 4616a06

Browse files
authored
Optimize Macro.classify_atom (#15768)
Atoms made exclusively of ASCII letters, numbers and underscores, beginning with a lowercase letter or underscore and optionally ending with ? or !, are always identifiers, and only atoms prefixed by "Elixir" are candidates for aliases. Recognizing both directly on the binary avoids building a charlist and running the unicode aware identifier tokenizer, which dominates the cost of classifying keyword list, map and struct keys when inspecting them.
1 parent 6cde868 commit 4616a06

1 file changed

Lines changed: 55 additions & 33 deletions

File tree

lib/elixir/lib/macro.ex

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,54 +2683,76 @@ defmodule Macro do
26832683
:unquoted_operator
26842684

26852685
true ->
2686-
charlist = Atom.to_charlist(atom)
2686+
classify_binary(Atom.to_string(atom), atom)
2687+
end
2688+
end
26872689

2688-
if valid_alias?(charlist) do
2689-
:alias
2690-
else
2691-
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
2692-
{kind, _acc, [], _, _, special} ->
2693-
cond do
2694-
kind != :identifier or :lists.member(:at, special) ->
2695-
:not_callable
2696-
2697-
# identifier_tokenizer used to return errors for non-nfc, but
2698-
# now it nfc-normalizes everything. However, lack of nfc is
2699-
# still a good reason to quote an atom when printing.
2700-
:lists.member(:nfkc, special) ->
2701-
:other
2702-
2703-
true ->
2704-
:identifier
2705-
end
2706-
2707-
_ ->
2708-
:other
2709-
end
2690+
# ASCII identifiers and aliases are recognized on the binary to avoid building a
2691+
# charlist and running the (unicode aware) tokenizer, which dominates the cost of
2692+
# classifying keyword list, map and struct keys.
2693+
defp classify_binary(<<char, rest::binary>>, atom)
2694+
when char >= ?a and char <= ?z
2695+
when char == ?_ do
2696+
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom)
2697+
end
2698+
2699+
defp classify_binary("Elixir" <> rest, atom) do
2700+
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom)
2701+
end
2702+
2703+
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom)
2704+
2705+
defp classify_with_tokenizer(atom) do
2706+
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do
2707+
{kind, _acc, [], _, _, special} ->
2708+
cond do
2709+
kind != :identifier or :lists.member(:at, special) ->
2710+
:not_callable
2711+
2712+
# identifier_tokenizer used to return errors for non-nfc, but
2713+
# now it nfc-normalizes everything. However, lack of nfc is
2714+
# still a good reason to quote an atom when printing.
2715+
:lists.member(:nfkc, special) ->
2716+
:other
2717+
2718+
true ->
2719+
:identifier
27102720
end
2721+
2722+
_ ->
2723+
:other
27112724
end
27122725
end
27132726

2714-
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
2715-
defp valid_alias?(_other), do: false
2727+
defp valid_identifier_rest?(<<char, rest::binary>>)
2728+
when char >= ?a and char <= ?z
2729+
when char >= ?A and char <= ?Z
2730+
when char >= ?0 and char <= ?9
2731+
when char == ?_ do
2732+
valid_identifier_rest?(rest)
2733+
end
2734+
2735+
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true
2736+
defp valid_identifier_rest?(<<>>), do: true
2737+
defp valid_identifier_rest?(_other), do: false
27162738

2717-
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
2718-
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
2739+
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z,
2740+
do: valid_alias_piece_rest?(rest)
27192741

2720-
defp valid_alias_piece?([]), do: true
2742+
defp valid_alias_piece?(<<>>), do: true
27212743
defp valid_alias_piece?(_other), do: false
27222744

2723-
defp trim_leading_while_valid_identifier([char | rest])
2745+
# A helper returning the rest of the binary would build a sub binary per piece,
2746+
# so branch back into valid_alias_piece?/1 to keep the match context.
2747+
defp valid_alias_piece_rest?(<<char, rest::binary>>)
27242748
when char >= ?a and char <= ?z
27252749
when char >= ?A and char <= ?Z
27262750
when char >= ?0 and char <= ?9
27272751
when char == ?_ do
2728-
trim_leading_while_valid_identifier(rest)
2752+
valid_alias_piece_rest?(rest)
27292753
end
27302754

2731-
defp trim_leading_while_valid_identifier(other) do
2732-
other
2733-
end
2755+
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other)
27342756

27352757
@doc """
27362758
Default backend for `Kernel.dbg/2`.

0 commit comments

Comments
 (0)