From eede7e696221a88ff24dca72f5f27fb9f2bfa238 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Tue, 18 Aug 2026 21:43:27 +0200 Subject: [PATCH 1/4] Classify ASCII identifiers and aliases on the binary in Macro 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. Both are matched at once because the first byte tells them apart and matching them separately would set up and throw away a second match context. For the same reason the alias scan branches between two states instead of returning the rest of the binary from a helper, which would build a sub binary per piece. The special form and quoted operator atoms move into guards so that no binary is built for atoms that are not callable. Inspecting a keyword list of 8 pairs goes from 10.4us to 8.9us (1.17x), a list of 8 module atoms from 4.0us to 3.5us (1.15x). Atoms that still fall through to the tokenizer pay ~5-10% for the extra scan. Co-Authored-By: Claude Opus 5 (1M context) --- lib/elixir/lib/macro.ex | 116 +++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index b687e5b35fe..8062dba1c98 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -207,6 +207,16 @@ defmodule Macro do escape: (binary(), char() -> binary()) ] + # Not a defguardp because Macro is compiled before Enum during bootstrap, + # and Kernel.defguard/1 relies on Enum. + defmacrop is_ascii_identifier_char(char) do + quote do + (unquote(char) >= ?a and unquote(char) <= ?z) or + (unquote(char) >= ?A and unquote(char) <= ?Z) or + (unquote(char) >= ?0 and unquote(char) <= ?9) or unquote(char) == ?_ + end + end + @doc """ Breaks a pipeline expression into a list. @@ -2670,67 +2680,77 @@ defmodule Macro do # * `:other` - any other atom (these are usually escaped when inspected, like # `:"foo and bar"`) # - defp inner_classify(atom) when is_atom(atom) do - cond do - atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] -> - :not_callable + defp inner_classify(atom) + when atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->], + do: :not_callable - # <|>, ^^^, and ~~~ are deprecated - atom in [:"::", :"^^^", :"~~~", :"<|>"] -> - :quoted_operator + # <|>, ^^^, and ~~~ are deprecated + defp inner_classify(atom) when atom in [:"::", :"^^^", :"~~~", :"<|>"], + do: :quoted_operator - operator?(atom, 1) or operator?(atom, 2) -> - :unquoted_operator + defp inner_classify(atom) when is_atom(atom) do + if operator?(atom, 1) or operator?(atom, 2) do + :unquoted_operator + else + # ASCII identifiers and aliases are recognized on the binary to avoid building a + # charlist and running the (unicode aware) tokenizer, which dominates the cost of + # classifying keyword list, map and struct keys. Both are matched at once because + # the first byte tells them apart and a second match would build a second context. + case Atom.to_string(atom) do + <> when (char >= ?a and char <= ?z) or char == ?_ -> + if ascii_identifier_rest?(rest), do: :identifier, else: tokenizer_classify(atom) + + "Elixir" <> rest -> + if valid_alias_piece?(rest), do: :alias, else: tokenizer_classify(atom) + + _other -> + tokenizer_classify(atom) + end + end + end - true -> - charlist = Atom.to_charlist(atom) + defp tokenizer_classify(atom) do + case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do + {kind, _acc, [], _, _, special} -> + cond do + kind != :identifier or :lists.member(:at, special) -> + :not_callable - if valid_alias?(charlist) do - :alias - else - case :elixir_config.identifier_tokenizer().tokenize(charlist) do - {kind, _acc, [], _, _, special} -> - cond do - kind != :identifier or :lists.member(:at, special) -> - :not_callable - - # identifier_tokenizer used to return errors for non-nfc, but - # now it nfc-normalizes everything. However, lack of nfc is - # still a good reason to quote an atom when printing. - :lists.member(:nfkc, special) -> - :other - - true -> - :identifier - end - - _ -> - :other - end + # identifier_tokenizer used to return errors for non-nfc, but + # now it nfc-normalizes everything. However, lack of nfc is + # still a good reason to quote an atom when printing. + :lists.member(:nfkc, special) -> + :other + + true -> + :identifier end + + _ -> + :other end end - defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest) - defp valid_alias?(_other), do: false + defp ascii_identifier_rest?(<>) when is_ascii_identifier_char(char), + do: ascii_identifier_rest?(rest) - defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z, - do: valid_alias_piece?(trim_leading_while_valid_identifier(rest)) + defp ascii_identifier_rest?(<>) when char == ?? or char == ?!, do: true + defp ascii_identifier_rest?(<<>>), do: true + defp ascii_identifier_rest?(_binary), do: false + + defp valid_alias_piece?(<<>>), do: true + + defp valid_alias_piece?(<>) when char >= ?A and char <= ?Z, + do: valid_alias_piece_rest?(rest) - defp valid_alias_piece?([]), do: true defp valid_alias_piece?(_other), do: false - defp trim_leading_while_valid_identifier([char | rest]) - when char >= ?a and char <= ?z - when char >= ?A and char <= ?Z - when char >= ?0 and char <= ?9 - when char == ?_ do - trim_leading_while_valid_identifier(rest) - end + # Inside a piece. A helper returning the rest of the binary would build a sub binary + # per piece, so branch back into valid_alias_piece?/1 to keep the match context. + defp valid_alias_piece_rest?(<>) when is_ascii_identifier_char(char), + do: valid_alias_piece_rest?(rest) - defp trim_leading_while_valid_identifier(other) do - other - end + defp valid_alias_piece_rest?(rest), do: valid_alias_piece?(rest) @doc """ Default backend for `Kernel.dbg/2`. From bf4793f3fe29c73d9bc3637affe578cd6aded736 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Tue, 18 Aug 2026 23:02:47 +0200 Subject: [PATCH 2/4] Add Macro.classify_atom/1 tests for identifier and alias boundaries The classification of ASCII identifiers and aliases is boundary heavy: only one trailing ? or !, only at the end, and the "Elixir" prefix is an alias only when every following piece starts with an uppercase letter. None of it was covered, so add the cases that separate identifiers and aliases from atoms that merely look like them. These pass before and after the binary classification fast path, pinning it to the tokenizer's verdict. Co-Authored-By: Claude Opus 5 (1M context) --- lib/elixir/test/elixir/macro_test.exs | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index fe992ba33ff..fb73b976dbc 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -1883,6 +1883,77 @@ defmodule MacroTest do end end + describe "classify_atom/1" do + test "identifiers" do + assert Macro.classify_atom(:foo) == :identifier + assert Macro.classify_atom(:foo_bar) == :identifier + assert Macro.classify_atom(:foo123) == :identifier + assert Macro.classify_atom(:fOO) == :identifier + assert Macro.classify_atom(:_) == :identifier + assert Macro.classify_atom(:_foo) == :identifier + assert Macro.classify_atom(:__foo__) == :identifier + assert Macro.classify_atom(:foo?) == :identifier + assert Macro.classify_atom(:foo!) == :identifier + assert Macro.classify_atom(:_foo?) == :identifier + assert Macro.classify_atom(:olá) == :identifier + assert Macro.classify_atom(:こんにちは世界) == :identifier + end + + test "identifiers with ? or ! in invalid positions" do + assert Macro.classify_atom(:"foo!!") == :quoted + assert Macro.classify_atom(:"foo??") == :quoted + assert Macro.classify_atom(:"foo?!") == :quoted + assert Macro.classify_atom(:"foo?bar") == :quoted + assert Macro.classify_atom(:"foo!bar") == :quoted + assert Macro.classify_atom(:"!foo") == :quoted + assert Macro.classify_atom(:"?foo") == :quoted + end + + test "aliases" do + assert Macro.classify_atom(Foo) == :alias + assert Macro.classify_atom(Foo.Bar) == :alias + assert Macro.classify_atom(Foo.Bar.Baz) == :alias + assert Macro.classify_atom(:"Elixir") == :alias + assert Macro.classify_atom(Elixir.Elixir) == :alias + assert Macro.classify_atom(:"Elixir.A1_b.C2") == :alias + end + + test "atoms prefixed by Elixir that are not aliases" do + assert Macro.classify_atom(:Elixirfoo) == :unquoted + assert Macro.classify_atom(:ElixirFoo) == :unquoted + assert Macro.classify_atom(:"Elixir.") == :quoted + assert Macro.classify_atom(:"Elixir.Foo.") == :quoted + assert Macro.classify_atom(:"Elixir.foo") == :quoted + assert Macro.classify_atom(:"Elixir.foo.Bar") == :quoted + assert Macro.classify_atom(:"Elixir.Foo.bar") == :quoted + assert Macro.classify_atom(:"Elixir.Foo..Bar") == :quoted + assert Macro.classify_atom(:"Elixir.1Foo") == :quoted + end + + test "unquoted" do + assert Macro.classify_atom(:Foo) == :unquoted + assert Macro.classify_atom(:FOO) == :unquoted + assert Macro.classify_atom(:foo@bar) == :unquoted + assert Macro.classify_atom(:+) == :unquoted + assert Macro.classify_atom(:...) == :unquoted + assert Macro.classify_atom(:..) == :unquoted + end + + test "quoted" do + assert Macro.classify_atom(:"::") == :quoted + assert Macro.classify_atom(:"^^^") == :quoted + assert Macro.classify_atom(:"@foo") == :quoted + assert Macro.classify_atom(:"foo bar") == :quoted + assert Macro.classify_atom(:"1foo") == :quoted + assert Macro.classify_atom(:"foo\n") == :quoted + assert Macro.classify_atom(:"") == :quoted + assert Macro.classify_atom(:" ") == :quoted + + nfd = :unicode.characters_to_nfd_binary("olá") + assert Macro.classify_atom(String.to_unsafe_atom(nfd)) == :quoted + end + end + test "operator?/2" do assert Macro.operator?(:+, 2) assert Macro.operator?(:+, 1) From 8451b761d7df5886ff86b1cf7002fd552954b3d7 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Tue, 18 Aug 2026 23:30:40 +0200 Subject: [PATCH 3/4] add edge cases to tests --- lib/elixir/test/elixir/macro_test.exs | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index fb73b976dbc..1678ae9d8d8 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -1897,6 +1897,18 @@ defmodule MacroTest do assert Macro.classify_atom(:_foo?) == :identifier assert Macro.classify_atom(:olá) == :identifier assert Macro.classify_atom(:こんにちは世界) == :identifier + assert Macro.classify_atom(:olá?) == :identifier + assert Macro.classify_atom(:olá!) == :identifier + assert Macro.classify_atom(:日本語) == :identifier + end + + test "reserved words are identifiers" do + assert Macro.classify_atom(true) == :identifier + assert Macro.classify_atom(false) == :identifier + assert Macro.classify_atom(nil) == :identifier + assert Macro.classify_atom(:do) == :identifier + assert Macro.classify_atom(:end) == :identifier + assert Macro.classify_atom(:fn) == :identifier end test "identifiers with ? or ! in invalid positions" do @@ -1937,17 +1949,54 @@ defmodule MacroTest do assert Macro.classify_atom(:+) == :unquoted assert Macro.classify_atom(:...) == :unquoted assert Macro.classify_atom(:..) == :unquoted + assert Macro.classify_atom(:foo@) == :unquoted + assert Macro.classify_atom(:_foo@bar) == :unquoted + assert Macro.classify_atom(:olá@bar) == :unquoted + assert Macro.classify_atom(:+) == :unquoted + assert Macro.classify_atom(:...) == :unquoted + assert Macro.classify_atom(:..) == :unquoted + + # atoms starting with a non-ASCII uppercase letter are not aliases + assert Macro.classify_atom(:Ólá) == :unquoted + assert Macro.classify_atom(:ΑΒΓ) == :unquoted + + # mixed scripts separated by an underscore tokenize as an alias + assert Macro.classify_atom(:T_シャツ) == :unquoted + end + + test "textual operators are unquoted, not identifiers" do + assert Macro.classify_atom(:when) == :unquoted + assert Macro.classify_atom(:and) == :unquoted + assert Macro.classify_atom(:or) == :unquoted + assert Macro.classify_atom(:not) == :unquoted + assert Macro.classify_atom(:in) == :unquoted + end + + test "special forms are unquoted, not callable" do + assert Macro.classify_atom(:%) == :unquoted + assert Macro.classify_atom(:%{}) == :unquoted + assert Macro.classify_atom(:{}) == :unquoted + assert Macro.classify_atom(:<<>>) == :unquoted + assert Macro.classify_atom(:.) == :unquoted + assert Macro.classify_atom(:->) == :unquoted + assert Macro.classify_atom(:..//) == :unquoted end test "quoted" do assert Macro.classify_atom(:"::") == :quoted assert Macro.classify_atom(:"^^^") == :quoted + assert Macro.classify_atom(:"~~~") == :quoted + assert Macro.classify_atom(:"<|>") == :quoted assert Macro.classify_atom(:"@foo") == :quoted assert Macro.classify_atom(:"foo bar") == :quoted assert Macro.classify_atom(:"1foo") == :quoted assert Macro.classify_atom(:"foo\n") == :quoted assert Macro.classify_atom(:"") == :quoted assert Macro.classify_atom(:" ") == :quoted + assert Macro.classify_atom(:"🌢") == :quoted + + # mixed-script identifiers cannot be tokenized + assert Macro.classify_atom(String.to_atom("аdmin")) == :quoted nfd = :unicode.characters_to_nfd_binary("olá") assert Macro.classify_atom(String.to_unsafe_atom(nfd)) == :quoted From e74fb567925c258fd2fa7c32ee6ca64919aa8503 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Wed, 19 Aug 2026 17:18:54 +0200 Subject: [PATCH 4/4] Address review feedback * Keep inner_classify/1 as a cond, changing only the fallback branch. The fast path moves into classify_binary/2, so the guards live in function heads instead of the cond. * Drop the classify_atom/1 tests, the behaviour is already covered by proxy elsewhere. Assisted-by: Claude Opus 5 --- lib/elixir/lib/macro.ex | 96 +++++++++++---------- lib/elixir/test/elixir/macro_test.exs | 120 -------------------------- 2 files changed, 49 insertions(+), 167 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 8062dba1c98..6d84ba04e8d 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -207,16 +207,6 @@ defmodule Macro do escape: (binary(), char() -> binary()) ] - # Not a defguardp because Macro is compiled before Enum during bootstrap, - # and Kernel.defguard/1 relies on Enum. - defmacrop is_ascii_identifier_char(char) do - quote do - (unquote(char) >= ?a and unquote(char) <= ?z) or - (unquote(char) >= ?A and unquote(char) <= ?Z) or - (unquote(char) >= ?0 and unquote(char) <= ?9) or unquote(char) == ?_ - end - end - @doc """ Breaks a pipeline expression into a list. @@ -2680,36 +2670,39 @@ defmodule Macro do # * `:other` - any other atom (these are usually escaped when inspected, like # `:"foo and bar"`) # - defp inner_classify(atom) - when atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->], - do: :not_callable + defp inner_classify(atom) when is_atom(atom) do + cond do + atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] -> + :not_callable - # <|>, ^^^, and ~~~ are deprecated - defp inner_classify(atom) when atom in [:"::", :"^^^", :"~~~", :"<|>"], - do: :quoted_operator + # <|>, ^^^, and ~~~ are deprecated + atom in [:"::", :"^^^", :"~~~", :"<|>"] -> + :quoted_operator - defp inner_classify(atom) when is_atom(atom) do - if operator?(atom, 1) or operator?(atom, 2) do - :unquoted_operator - else - # ASCII identifiers and aliases are recognized on the binary to avoid building a - # charlist and running the (unicode aware) tokenizer, which dominates the cost of - # classifying keyword list, map and struct keys. Both are matched at once because - # the first byte tells them apart and a second match would build a second context. - case Atom.to_string(atom) do - <> when (char >= ?a and char <= ?z) or char == ?_ -> - if ascii_identifier_rest?(rest), do: :identifier, else: tokenizer_classify(atom) - - "Elixir" <> rest -> - if valid_alias_piece?(rest), do: :alias, else: tokenizer_classify(atom) - - _other -> - tokenizer_classify(atom) - end + operator?(atom, 1) or operator?(atom, 2) -> + :unquoted_operator + + true -> + classify_binary(Atom.to_string(atom), atom) end end - defp tokenizer_classify(atom) do + # ASCII identifiers and aliases are recognized on the binary to avoid building a + # charlist and running the (unicode aware) tokenizer, which dominates the cost of + # classifying keyword list, map and struct keys. + defp classify_binary(<>, atom) + when char >= ?a and char <= ?z + when char == ?_ do + if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom) + end + + defp classify_binary("Elixir" <> rest, atom) do + if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom) + end + + defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom) + + defp classify_with_tokenizer(atom) do case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do {kind, _acc, [], _, _, special} -> cond do @@ -2731,26 +2724,35 @@ defmodule Macro do end end - defp ascii_identifier_rest?(<>) when is_ascii_identifier_char(char), - do: ascii_identifier_rest?(rest) - - defp ascii_identifier_rest?(<>) when char == ?? or char == ?!, do: true - defp ascii_identifier_rest?(<<>>), do: true - defp ascii_identifier_rest?(_binary), do: false + defp valid_identifier_rest?(<>) + when char >= ?a and char <= ?z + when char >= ?A and char <= ?Z + when char >= ?0 and char <= ?9 + when char == ?_ do + valid_identifier_rest?(rest) + end - defp valid_alias_piece?(<<>>), do: true + defp valid_identifier_rest?(<>) when char == ?? when char == ?!, do: true + defp valid_identifier_rest?(<<>>), do: true + defp valid_identifier_rest?(_other), do: false defp valid_alias_piece?(<>) when char >= ?A and char <= ?Z, do: valid_alias_piece_rest?(rest) + defp valid_alias_piece?(<<>>), do: true defp valid_alias_piece?(_other), do: false - # Inside a piece. A helper returning the rest of the binary would build a sub binary - # per piece, so branch back into valid_alias_piece?/1 to keep the match context. - defp valid_alias_piece_rest?(<>) when is_ascii_identifier_char(char), - do: valid_alias_piece_rest?(rest) + # A helper returning the rest of the binary would build a sub binary per piece, + # so branch back into valid_alias_piece?/1 to keep the match context. + defp valid_alias_piece_rest?(<>) + when char >= ?a and char <= ?z + when char >= ?A and char <= ?Z + when char >= ?0 and char <= ?9 + when char == ?_ do + valid_alias_piece_rest?(rest) + end - defp valid_alias_piece_rest?(rest), do: valid_alias_piece?(rest) + defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other) @doc """ Default backend for `Kernel.dbg/2`. diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 1678ae9d8d8..fe992ba33ff 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -1883,126 +1883,6 @@ defmodule MacroTest do end end - describe "classify_atom/1" do - test "identifiers" do - assert Macro.classify_atom(:foo) == :identifier - assert Macro.classify_atom(:foo_bar) == :identifier - assert Macro.classify_atom(:foo123) == :identifier - assert Macro.classify_atom(:fOO) == :identifier - assert Macro.classify_atom(:_) == :identifier - assert Macro.classify_atom(:_foo) == :identifier - assert Macro.classify_atom(:__foo__) == :identifier - assert Macro.classify_atom(:foo?) == :identifier - assert Macro.classify_atom(:foo!) == :identifier - assert Macro.classify_atom(:_foo?) == :identifier - assert Macro.classify_atom(:olá) == :identifier - assert Macro.classify_atom(:こんにちは世界) == :identifier - assert Macro.classify_atom(:olá?) == :identifier - assert Macro.classify_atom(:olá!) == :identifier - assert Macro.classify_atom(:日本語) == :identifier - end - - test "reserved words are identifiers" do - assert Macro.classify_atom(true) == :identifier - assert Macro.classify_atom(false) == :identifier - assert Macro.classify_atom(nil) == :identifier - assert Macro.classify_atom(:do) == :identifier - assert Macro.classify_atom(:end) == :identifier - assert Macro.classify_atom(:fn) == :identifier - end - - test "identifiers with ? or ! in invalid positions" do - assert Macro.classify_atom(:"foo!!") == :quoted - assert Macro.classify_atom(:"foo??") == :quoted - assert Macro.classify_atom(:"foo?!") == :quoted - assert Macro.classify_atom(:"foo?bar") == :quoted - assert Macro.classify_atom(:"foo!bar") == :quoted - assert Macro.classify_atom(:"!foo") == :quoted - assert Macro.classify_atom(:"?foo") == :quoted - end - - test "aliases" do - assert Macro.classify_atom(Foo) == :alias - assert Macro.classify_atom(Foo.Bar) == :alias - assert Macro.classify_atom(Foo.Bar.Baz) == :alias - assert Macro.classify_atom(:"Elixir") == :alias - assert Macro.classify_atom(Elixir.Elixir) == :alias - assert Macro.classify_atom(:"Elixir.A1_b.C2") == :alias - end - - test "atoms prefixed by Elixir that are not aliases" do - assert Macro.classify_atom(:Elixirfoo) == :unquoted - assert Macro.classify_atom(:ElixirFoo) == :unquoted - assert Macro.classify_atom(:"Elixir.") == :quoted - assert Macro.classify_atom(:"Elixir.Foo.") == :quoted - assert Macro.classify_atom(:"Elixir.foo") == :quoted - assert Macro.classify_atom(:"Elixir.foo.Bar") == :quoted - assert Macro.classify_atom(:"Elixir.Foo.bar") == :quoted - assert Macro.classify_atom(:"Elixir.Foo..Bar") == :quoted - assert Macro.classify_atom(:"Elixir.1Foo") == :quoted - end - - test "unquoted" do - assert Macro.classify_atom(:Foo) == :unquoted - assert Macro.classify_atom(:FOO) == :unquoted - assert Macro.classify_atom(:foo@bar) == :unquoted - assert Macro.classify_atom(:+) == :unquoted - assert Macro.classify_atom(:...) == :unquoted - assert Macro.classify_atom(:..) == :unquoted - assert Macro.classify_atom(:foo@) == :unquoted - assert Macro.classify_atom(:_foo@bar) == :unquoted - assert Macro.classify_atom(:olá@bar) == :unquoted - assert Macro.classify_atom(:+) == :unquoted - assert Macro.classify_atom(:...) == :unquoted - assert Macro.classify_atom(:..) == :unquoted - - # atoms starting with a non-ASCII uppercase letter are not aliases - assert Macro.classify_atom(:Ólá) == :unquoted - assert Macro.classify_atom(:ΑΒΓ) == :unquoted - - # mixed scripts separated by an underscore tokenize as an alias - assert Macro.classify_atom(:T_シャツ) == :unquoted - end - - test "textual operators are unquoted, not identifiers" do - assert Macro.classify_atom(:when) == :unquoted - assert Macro.classify_atom(:and) == :unquoted - assert Macro.classify_atom(:or) == :unquoted - assert Macro.classify_atom(:not) == :unquoted - assert Macro.classify_atom(:in) == :unquoted - end - - test "special forms are unquoted, not callable" do - assert Macro.classify_atom(:%) == :unquoted - assert Macro.classify_atom(:%{}) == :unquoted - assert Macro.classify_atom(:{}) == :unquoted - assert Macro.classify_atom(:<<>>) == :unquoted - assert Macro.classify_atom(:.) == :unquoted - assert Macro.classify_atom(:->) == :unquoted - assert Macro.classify_atom(:..//) == :unquoted - end - - test "quoted" do - assert Macro.classify_atom(:"::") == :quoted - assert Macro.classify_atom(:"^^^") == :quoted - assert Macro.classify_atom(:"~~~") == :quoted - assert Macro.classify_atom(:"<|>") == :quoted - assert Macro.classify_atom(:"@foo") == :quoted - assert Macro.classify_atom(:"foo bar") == :quoted - assert Macro.classify_atom(:"1foo") == :quoted - assert Macro.classify_atom(:"foo\n") == :quoted - assert Macro.classify_atom(:"") == :quoted - assert Macro.classify_atom(:" ") == :quoted - assert Macro.classify_atom(:"🌢") == :quoted - - # mixed-script identifiers cannot be tokenized - assert Macro.classify_atom(String.to_atom("аdmin")) == :quoted - - nfd = :unicode.characters_to_nfd_binary("olá") - assert Macro.classify_atom(String.to_unsafe_atom(nfd)) == :quoted - end - end - test "operator?/2" do assert Macro.operator?(:+, 2) assert Macro.operator?(:+, 1)