-module(elixir_tokenizer). -include("elixir.hrl"). -export([tokenize/1, tokenize/3, tokenize/4, invalid_do_error/1]). %% Numbers -define(is_hex(S), (?is_digit(S) orelse (S >= $A andalso S =< $F) orelse (S >= $a andalso S =< $f))). -define(is_bin(S), (S >= $0 andalso S =< $1)). -define(is_octal(S), (S >= $0 andalso S =< $7)). %% Digits and letters -define(is_digit(S), (S >= $0 andalso S =< $9)). -define(is_upcase(S), (S >= $A andalso S =< $Z)). -define(is_downcase(S), (S >= $a andalso S =< $z)). %% Others -define(is_quote(S), (S == $" orelse S == $')). -define(is_sigil(S), ((S == $/) orelse (S == $<) orelse (S == $") orelse (S == $') orelse (S == $[) orelse (S == $() orelse (S == ${) orelse (S == $|))). %% Spaces -define(is_horizontal_space(S), ((S == $\s) orelse (S == $\t))). -define(is_vertical_space(S), ((S == $\r) orelse (S == $\n))). -define(is_space(S), (?is_horizontal_space(S) orelse ?is_vertical_space(S))). %% Operators -define(at_op(T), T == $@). -define(capture_op(T), T == $&). -define(unary_op(T), T == $!; T == $^). -define(unary_op3(T1, T2, T3), T1 == $~, T2 == $~, T3 == $~). -define(two_op(T1, T2), T1 == $+, T2 == $+; T1 == $-, T2 == $-; T1 == $<, T2 == $>; T1 == $., T2 == $.). -define(three_op(T1, T2, T3), T1 == $^, T2 == $^, T3 == $^). -define(mult_op(T), T == $* orelse T == $/). -define(dual_op(T), T == $+ orelse T == $-). -define(arrow_op3(T1, T2, T3), T1 == $<, T2 == $<, T3 == $<; T1 == $>, T2 == $>, T3 == $>; T1 == $~, T2 == $>, T3 == $>; T1 == $<, T2 == $<, T3 == $~; T1 == $<, T2 == $~, T3 == $>; T1 == $<, T2 == $|, T3 == $>). -define(arrow_op(T1, T2), T1 == $|, T2 == $>; T1 == $~, T2 == $>; T1 == $<, T2 == $~). -define(rel_op(T), T == $<; T == $>). -define(rel_op2(T1, T2), T1 == $<, T2 == $=; T1 == $>, T2 == $=). -define(comp_op2(T1, T2), T1 == $=, T2 == $=; T1 == $=, T2 == $~; T1 == $!, T2 == $=). -define(comp_op3(T1, T2, T3), T1 == $=, T2 == $=, T3 == $=; T1 == $!, T2 == $=, T3 == $=). -define(and_op(T1, T2), T1 == $&, T2 == $&). -define(or_op(T1, T2), T1 == $|, T2 == $|). -define(and_op3(T1, T2, T3), T1 == $&, T2 == $&, T3 == $&). -define(or_op3(T1, T2, T3), T1 == $|, T2 == $|, T3 == $|). -define(match_op(T), T == $=). -define(in_match_op(T1, T2), T1 == $<, T2 == $-; T1 == $\\, T2 == $\\). -define(stab_op(T1, T2), T1 == $-, T2 == $>). -define(type_op(T1, T2), T1 == $:, T2 == $:). -define(pipe_op(T), T == $|). tokenize(String, Line, Column, #elixir_tokenizer{} = Scope) -> tokenize(String, Line, Column, Scope, []); tokenize(String, Line, Column, Opts) -> IdentifierTokenizer = elixir_config:safe_get(identifier_tokenizer, 'Elixir.String.Tokenizer'), Scope = lists:foldl(fun ({file, File}, Acc) when is_binary(File) -> Acc#elixir_tokenizer{file=File}; ({existing_atoms_only, ExistingAtomsOnly}, Acc) when is_boolean(ExistingAtomsOnly) -> Acc#elixir_tokenizer{existing_atoms_only=ExistingAtomsOnly}; ({check_terminators, CheckTerminators}, Acc) when is_boolean(CheckTerminators) -> Acc#elixir_tokenizer{check_terminators=CheckTerminators}; ({preserve_comments, PreserveComments}, Acc) when is_function(PreserveComments) -> Acc#elixir_tokenizer{preserve_comments=PreserveComments}; ({unescape, Unescape}, Acc) when is_boolean(Unescape) -> Acc#elixir_tokenizer{unescape=Unescape}; (_, Acc) -> Acc end, #elixir_tokenizer{identifier_tokenizer=IdentifierTokenizer}, Opts), tokenize(String, Line, Column, Scope, []). tokenize(String, Line, Opts) -> tokenize(String, Line, 1, Opts). tokenize([], _Line, _Column, #elixir_tokenizer{terminators=[]}, Tokens) -> {ok, lists:reverse(Tokens)}; tokenize([], EndLine, _Column, #elixir_tokenizer{terminators=[{Start, {StartLine, _, _}} | _]}, Tokens) -> End = terminator(Start), Message = io_lib:format("missing terminator: ~ts (for \"~ts\" starting at line ~B)", [End, Start, StartLine]), {error, {EndLine, Message, []}, [], Tokens}; % VC merge conflict tokenize(("<<<<<<<" ++ _) = Original, Line, 1, _Scope, Tokens) -> FirstLine = lists:takewhile(fun(C) -> C =/= $\n andalso C =/= $\r end, Original), {error, {Line, "found an unexpected version control marker, please resolve the conflicts: ", FirstLine}, Original, Tokens}; % Base integers tokenize([$0, $x, H | T], Line, Column, Scope, Tokens) when ?is_hex(H) -> {Rest, Number, OriginalRepresentation, Length} = tokenize_hex(T, [H], 1), Token = {int, {Line, Column, Number}, OriginalRepresentation}, tokenize(Rest, Line, Column + 2 + Length, Scope, [Token | Tokens]); tokenize([$0, $b, H | T], Line, Column, Scope, Tokens) when ?is_bin(H) -> {Rest, Number, OriginalRepresentation, Length} = tokenize_bin(T, [H], 1), Token = {int, {Line, Column, Number}, OriginalRepresentation}, tokenize(Rest, Line, Column + 2 + Length, Scope, [Token | Tokens]); tokenize([$0, $o, H | T], Line, Column, Scope, Tokens) when ?is_octal(H) -> {Rest, Number, OriginalRepresentation, Length} = tokenize_octal(T, [H], 1), Token = {int, {Line, Column, Number}, OriginalRepresentation}, tokenize(Rest, Line, Column + 2 + Length, Scope, [Token | Tokens]); % Comments tokenize([$# | String], Line, Column, Scope, Tokens) -> {Rest, Comment} = tokenize_comment(String, [$#]), preserve_comments(Line, Column, Tokens, Comment, Rest, Scope), tokenize(Rest, Line, Column, Scope, reset_eol(Tokens)); % Sigils tokenize([$~, S, H, H, H | T] = Original, Line, Column, Scope, Tokens) when ?is_quote(H), ?is_upcase(S) orelse ?is_downcase(S) -> case extract_heredoc_with_interpolation(Line, Column, Scope, ?is_downcase(S), T, H) of {ok, NewLine, NewColumn, Parts, Rest} -> {Final, Modifiers} = collect_modifiers(Rest, []), Token = {sigil, {Line, Column, nil}, S, Parts, Modifiers, <>}, tokenize(Final, NewLine, NewColumn, Scope, [Token | Tokens]); {error, Reason} -> {error, Reason, Original, Tokens} end; tokenize([$~, S, H | T] = Original, Line, Column, Scope, Tokens) when ?is_sigil(H), ?is_upcase(S) orelse ?is_downcase(S) -> case elixir_interpolation:extract(Line, Column + 3, Scope, ?is_downcase(S), T, sigil_terminator(H)) of {NewLine, NewColumn, Parts, Rest} -> {Final, Modifiers} = collect_modifiers(Rest, []), Token = {sigil, {Line, Column, nil}, S, Parts, Modifiers, <>}, tokenize(Final, NewLine, NewColumn, Scope, [Token | Tokens]); {error, Reason} -> Sigil = [$~, S, H], interpolation_error(Reason, Original, Tokens, " (for sigil ~ts starting at line ~B)", [Sigil, Line]) end; tokenize([$~, S, H | _] = Original, Line, Column, _Scope, Tokens) when ?is_upcase(S) orelse ?is_downcase(S) -> MessageString = "\"~ts\" (column ~p, codepoint U+~4.16.0B). The available delimiters are: " "//, ||, \"\", '', (), [], {}, <>", Message = io_lib:format(MessageString, [[H], Column + 2, H]), {error, {Line, "invalid sigil delimiter: ", Message}, Original, Tokens}; % Char tokens % We tokenize char literals (?a) as {char, _, CharInt} instead of {number, _, % CharInt}. This is exactly what Erlang does with Erlang char literals % ($a). This means we'll have to adjust the error message for char literals in % elixir_errors.erl as by default {char, _, _} tokens are "hijacked" by Erlang % and printed with Erlang syntax ($a) in the parser's error messages. tokenize([$?, $\\, H | T], Line, Column, Scope, Tokens) -> Char = elixir_interpolation:unescape_map(H), Token = {char, {Line, Column, [$?, $\\, H]}, Char}, tokenize(T, Line, Column + 3, Scope, [Token | Tokens]); tokenize([$?, Char | T], Line, Column, Scope, Tokens) -> case handle_char(Char) of {Escape, Name} -> Msg = io_lib:format("found ? followed by codepoint 0x~.16B (~ts), please use ~ts instead", [Char, Name, Escape]), elixir_errors:warn(Line, Scope#elixir_tokenizer.file, Msg); false -> ok end, Token = {char, {Line, Column, [$?, Char]}, Char}, tokenize(T, Line, Column + 2, Scope, [Token | Tokens]); % Heredocs tokenize("\"\"\"" ++ T, Line, Column, Scope, Tokens) -> handle_heredocs(T, Line, Column, $", Scope, Tokens); tokenize("'''" ++ T, Line, Column, Scope, Tokens) -> handle_heredocs(T, Line, Column, $', Scope, Tokens); % Strings tokenize([$" | T], Line, Column, Scope, Tokens) -> handle_strings(T, Line, Column + 1, $", Scope, Tokens); tokenize([$' | T], Line, Column, Scope, Tokens) -> handle_strings(T, Line, Column + 1, $', Scope, Tokens); % Operator atoms tokenize("...:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 4, Scope, [{kw_identifier, {Line, Column, nil}, '...'} | Tokens]); tokenize("<<>>:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 5, Scope, [{kw_identifier, {Line, Column, nil}, '<<>>'} | Tokens]); tokenize("%{}:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 4, Scope, [{kw_identifier, {Line, Column, nil}, '%{}'} | Tokens]); tokenize("%:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 2, Scope, [{kw_identifier, {Line, Column, nil}, '%'} | Tokens]); tokenize("{}:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 3, Scope, [{kw_identifier, {Line, Column, nil}, '{}'} | Tokens]); tokenize(":..." ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 4, Scope, [{atom, {Line, Column, nil}, '...'} | Tokens]); tokenize(":<<>>" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 5, Scope, [{atom, {Line, Column, nil}, '<<>>'} | Tokens]); tokenize(":%{}" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 4, Scope, [{atom, {Line, Column, nil}, '%{}'} | Tokens]); tokenize(":%" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 2, Scope, [{atom, {Line, Column, nil}, '%'} | Tokens]); tokenize(":{}" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 3, Scope, [{atom, {Line, Column, nil}, '{}'} | Tokens]); % ## Three Token Operators tokenize([$:, T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?unary_op3(T1, T2, T3); ?comp_op3(T1, T2, T3); ?and_op3(T1, T2, T3); ?or_op3(T1, T2, T3); ?arrow_op3(T1, T2, T3); ?three_op(T1, T2, T3) -> Token = {atom, {Line, Column, nil}, list_to_atom([T1, T2, T3])}, tokenize(Rest, Line, Column + 4, Scope, [Token | Tokens]); % ## Two Token Operators tokenize([$:, T1, T2 | Rest], Line, Column, Scope, Tokens) when ?comp_op2(T1, T2); ?rel_op2(T1, T2); ?and_op(T1, T2); ?or_op(T1, T2); ?arrow_op(T1, T2); ?in_match_op(T1, T2); ?two_op(T1, T2); ?stab_op(T1, T2); ?type_op(T1, T2) -> Token = {atom, {Line, Column, nil}, list_to_atom([T1, T2])}, tokenize(Rest, Line, Column + 3, Scope, [Token | Tokens]); % ## Single Token Operators tokenize([$:, T | Rest], Line, Column, Scope, Tokens) when ?at_op(T); ?unary_op(T); ?capture_op(T); ?dual_op(T); ?mult_op(T); ?rel_op(T); ?match_op(T); ?pipe_op(T); T == $. -> Token = {atom, {Line, Column, nil}, list_to_atom([T])}, tokenize(Rest, Line, Column + 2, Scope, [Token | Tokens]); % Stand-alone tokens tokenize("..." ++ Rest, Line, Column, Scope, Tokens) -> Token = check_call_identifier(Line, Column, '...', Rest), tokenize(Rest, Line, Column + 3, Scope, [Token | Tokens]); tokenize("=>" ++ Rest, Line, Column, Scope, Tokens) -> Token = {assoc_op, {Line, Column, previous_was_eol(Tokens)}, '=>'}, tokenize(Rest, Line, Column + 2, Scope, add_token_with_eol(Token, Tokens)); % ## Three token operators tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?unary_op3(T1, T2, T3) -> handle_unary_op(Rest, Line, Column, unary_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?comp_op3(T1, T2, T3) -> handle_op(Rest, Line, Column, comp_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?and_op3(T1, T2, T3) -> handle_op(Rest, Line, Column, and_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?or_op3(T1, T2, T3) -> handle_op(Rest, Line, Column, or_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?three_op(T1, T2, T3) -> handle_op(Rest, Line, Column, three_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?arrow_op3(T1, T2, T3) -> handle_op(Rest, Line, Column, arrow_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); % ## Containers + punctuation tokens tokenize([$, | Rest], Line, Column, Scope, Tokens) -> Token = {',', {Line, Column, 0}}, handle_terminator(Rest, Line, Column + 1, Scope, Token, Tokens); tokenize([$<, $< | Rest], Line, Column, Scope, Tokens) -> Token = {'<<', {Line, Column, nil}}, handle_terminator(Rest, Line, Column + 2, Scope, Token, Tokens); tokenize([$>, $> | Rest], Line, Column, Scope, Tokens) -> Token = {'>>', {Line, Column, previous_was_eol(Tokens)}}, handle_terminator(Rest, Line, Column + 2, Scope, Token, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when T == $(; T == ${; T == $[ -> Token = {list_to_atom([T]), {Line, Column, nil}}, handle_terminator(Rest, Line, Column + 1, Scope, Token, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when T == $); T == $}; T == $] -> Token = {list_to_atom([T]), {Line, Column, previous_was_eol(Tokens)}}, handle_terminator(Rest, Line, Column + 1, Scope, Token, Tokens); % ## Two Token Operators tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?two_op(T1, T2) -> handle_op(Rest, Line, Column, two_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?arrow_op(T1, T2) -> handle_op(Rest, Line, Column, arrow_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?comp_op2(T1, T2) -> handle_op(Rest, Line, Column, comp_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?rel_op2(T1, T2) -> handle_op(Rest, Line, Column, rel_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?and_op(T1, T2) -> handle_op(Rest, Line, Column, and_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?or_op(T1, T2) -> handle_op(Rest, Line, Column, or_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?in_match_op(T1, T2) -> handle_op(Rest, Line, Column, in_match_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?type_op(T1, T2) -> handle_op(Rest, Line, Column, type_op, 2, list_to_atom([T1, T2]), Scope, Tokens); tokenize([T1, T2 | Rest], Line, Column, Scope, Tokens) when ?stab_op(T1, T2) -> handle_op(Rest, Line, Column, stab_op, 2, list_to_atom([T1, T2]), Scope, Tokens); % ## Single Token Operators tokenize([T | Rest], Line, Column, Scope, Tokens) when ?at_op(T) -> handle_unary_op(Rest, Line, Column, at_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?capture_op(T) -> handle_unary_op(Rest, Line, Column, capture_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?unary_op(T) -> handle_unary_op(Rest, Line, Column, unary_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?rel_op(T) -> handle_op(Rest, Line, Column, rel_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?dual_op(T) -> handle_unary_op(Rest, Line, Column, dual_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?mult_op(T) -> handle_op(Rest, Line, Column, mult_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?match_op(T) -> handle_op(Rest, Line, Column, match_op, 1, list_to_atom([T]), Scope, Tokens); tokenize([T | Rest], Line, Column, Scope, Tokens) when ?pipe_op(T) -> handle_op(Rest, Line, Column, pipe_op, 1, list_to_atom([T]), Scope, Tokens); % Non-operator Atoms tokenize([$:, H | T] = Original, Line, Column, Scope, Tokens) when ?is_quote(H) -> case elixir_interpolation:extract(Line, Column + 2, Scope, true, T, H) of {NewLine, NewColumn, Parts, Rest} -> Unescaped = unescape_tokens(Parts, Scope), Key = case Scope#elixir_tokenizer.existing_atoms_only of true -> atom_safe; false -> atom_unsafe end, Token = {Key, {Line, Column, nil}, Unescaped}, tokenize(Rest, NewLine, NewColumn, Scope, [Token | Tokens]); {error, Reason} -> interpolation_error(Reason, Original, Tokens, " (for atom starting at line ~B)", [Line]) end; tokenize([$: | String] = Original, Line, Column, Scope, Tokens) -> case tokenize_identifier(String, Line, Scope) of {_Kind, Atom, Rest, Length, _Ascii, _Special} -> Token = {atom, {Line, Column, nil}, Atom}, tokenize(Rest, Line, Column + 1 + Length, Scope, [Token | Tokens]); empty -> unexpected_token(Original, Line, Column, Tokens); {error, Reason} -> {error, Reason, Original, Tokens} end; % Integers and floats tokenize([H | T], Line, Column, Scope, Tokens) when ?is_digit(H) -> case tokenize_number(T, [H], 1, false) of {error, Reason, Number} -> {error, {Line, Reason, Number}, T, Tokens}; {Rest, Number, Original, Length} when is_integer(Number) -> Token = {int, {Line, Column, Number}, Original}, tokenize(Rest, Line, Column + Length, Scope, [Token | Tokens]); {Rest, Number, Original, Length} -> Token = {float, {Line, Column, Number}, Original}, tokenize(Rest, Line, Column + Length, Scope, [Token | Tokens]) end; % Spaces tokenize([T | Rest], Line, Column, Scope, Tokens) when ?is_horizontal_space(T) -> {Remaining, Stripped} = strip_horizontal_space(Rest), handle_space_sensitive_tokens(Remaining, Line, Column + 1 + Stripped, Scope, Tokens); % End of line tokenize(";" ++ Rest, Line, Column, Scope, []) -> tokenize(Rest, Line, Column + 1, Scope, [{';', {Line, Column, 0}}]); tokenize(";" ++ Rest, Line, Column, Scope, [Top | _] = Tokens) when element(1, Top) /= ';' -> tokenize(Rest, Line, Column + 1, Scope, [{';', {Line, Column, 0}} | Tokens]); tokenize("\\" = Original, Line, _Column, _Scope, Tokens) -> {error, {Line, "invalid escape \\ at end of file", []}, Original, Tokens}; tokenize("\\\n" = Original, Line, _Column, _Scope, Tokens) -> {error, {Line, "invalid escape \\ at end of file", []}, Original, Tokens}; tokenize("\\\r\n" = Original, Line, _Column, _Scope, Tokens) -> {error, {Line, "invalid escape \\ at end of file", []}, Original, Tokens}; tokenize("\\\n" ++ Rest, Line, _Column, Scope, Tokens) -> tokenize(Rest, Line + 1, 1, Scope, Tokens); tokenize("\\\r\n" ++ Rest, Line, _Column, Scope, Tokens) -> tokenize(Rest, Line + 1, 1, Scope, Tokens); tokenize("\n" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line + 1, 1, Scope, eol(Line, Column, Tokens)); tokenize("\r\n" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line + 1, 1, Scope, eol(Line, Column, Tokens)); % Others tokenize([$%, ${ | T], Line, Column, Scope, Tokens) -> tokenize([${ | T], Line, Column + 1, Scope, [{'%{}', {Line, Column, nil}} | Tokens]); tokenize([$% | T], Line, Column, Scope, Tokens) -> tokenize(T, Line, Column + 1, Scope, [{'%', {Line, Column, nil}} | Tokens]); tokenize([$. | T], Line, Column, Scope, Tokens) -> DotInfo = {Line, Column, nil}, {Rest, EndLine, EndColumn} = strip_dot_space(T, Line, Column + 1, [{'.', DotInfo}| Tokens], Scope), handle_dot([$. | Rest], EndLine, EndColumn, DotInfo, Scope, Tokens); % Identifiers tokenize(String, Line, Column, Scope, Tokens) -> case tokenize_identifier(String, Line, Scope) of {Kind, Atom, Rest, Length, Ascii, Special} -> HasAt = lists:member($@, Special), case Rest of [$: | T] when ?is_space(hd(T)) -> Token = {kw_identifier, {Line, Column, nil}, Atom}, tokenize(T, Line, Column + Length + 1, Scope, [Token | Tokens]); [$: | T] when hd(T) /= $: -> AtomName = atom_to_list(Atom) ++ [$:], Reason = {Line, "keyword argument must be followed by space after: ", AtomName}, {error, Reason, String, Tokens}; _ when HasAt -> Reason = {Line, invalid_character_error(Kind, $@), atom_to_list(Atom)}, {error, Reason, String, Tokens}; _ when Kind == alias -> tokenize_alias(Rest, Line, Column, Atom, Length, Ascii, Special, Scope, Tokens); _ when Kind == identifier -> tokenize_other(Rest, Line, Column, Atom, Length, Scope, Tokens); _ -> unexpected_token(String, Line, Column, Tokens) end; empty -> unexpected_token(String, Line, Column, Tokens); {error, Reason} -> {error, Reason, String, Tokens} end. unexpected_token([T | Rest], Line, Column, Tokens) -> Message = io_lib:format("\"~ts\" (column ~p, codepoint U+~4.16.0B)", [[T], Column, T]), {error, {Line, "unexpected token: ", Message}, Rest, Tokens}. strip_horizontal_space(T) -> strip_horizontal_space(T, 0). strip_horizontal_space([H | T], Counter) when ?is_horizontal_space(H) -> strip_horizontal_space(T, Counter + 1); strip_horizontal_space(T, Counter) -> {T, Counter}. strip_dot_space(T, Line, Column, Tokens, Scope) -> case strip_horizontal_space(T) of {"#" ++ R, _} -> {Rest, Comment} = tokenize_comment(R, [$#]), preserve_comments(Line, Column, Tokens, Comment, Rest, Scope), strip_dot_space(Rest, Line, 1, reset_eol(Tokens), Scope); {"\r\n" ++ Rest, _} -> strip_dot_space(Rest, Line + 1, 1, eol(Line, Column, Tokens), Scope); {"\n" ++ Rest, _} -> strip_dot_space(Rest, Line + 1, 1, eol(Line, Column, Tokens), Scope); {Rest, Length} -> {Rest, Line, Column + Length} end. handle_char(7) -> {"\\a", "alert"}; handle_char($\b) -> {"\\b", "backspace"}; handle_char($\d) -> {"\\d", "delete"}; handle_char($\e) -> {"\\e", "escape"}; handle_char($\f) -> {"\\f", "form feed"}; handle_char($\n) -> {"\\n", "newline"}; handle_char($\r) -> {"\\r", "carriage return"}; handle_char($\s) -> {"\\s", "space"}; handle_char($\t) -> {"\\t", "tab"}; handle_char($\v) -> {"\\v", "vertical tab"}; handle_char(_) -> false. %% Handlers handle_heredocs(T, Line, Column, H, Scope, Tokens) -> case extract_heredoc_with_interpolation(Line, Column, Scope, true, T, H) of {ok, NewLine, NewColumn, Parts, Rest} -> Token = {heredoc_type(H), {Line, Column, nil}, unescape_tokens(Parts, Scope)}, tokenize(Rest, NewLine, NewColumn, Scope, [Token | Tokens]); {error, Reason} -> {error, Reason, [H, H, H] ++ T, Tokens} end. handle_strings(T, Line, Column, H, Scope, Tokens) -> case elixir_interpolation:extract(Line, Column, Scope, true, T, H) of {error, Reason} -> interpolation_error(Reason, [H | T], Tokens, " (for string starting at line ~B)", [Line]); {NewLine, NewColumn, Parts, [$: | Rest]} when ?is_space(hd(Rest)) -> Unescaped = unescape_tokens(Parts, Scope), Key = case Scope#elixir_tokenizer.existing_atoms_only of true -> kw_identifier_safe; false -> kw_identifier_unsafe end, Token = {Key, {Line, Column - 1, nil}, Unescaped}, tokenize(Rest, NewLine, NewColumn, Scope, [Token | Tokens]); {NewLine, NewColumn, Parts, Rest} -> Token = {string_type(H), {Line, Column - 1, nil}, unescape_tokens(Parts, Scope)}, tokenize(Rest, NewLine, NewColumn, Scope, [Token | Tokens]) end. handle_unary_op([$: | Rest], Line, Column, _Kind, Length, Op, Scope, Tokens) when ?is_space(hd(Rest)) -> Token = {kw_identifier, {Line, Column, nil}, Op}, tokenize(Rest, Line, Column + Length + 1, Scope, [Token | Tokens]); handle_unary_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) -> case strip_horizontal_space(Rest) of {[$/ | _] = Remaining, Extra} -> Token = {identifier, {Line, Column, nil}, Op}, tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens]); {Remaining, Extra} -> Token = {Kind, {Line, Column, nil}, Op}, tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens]) end. handle_op([$: | Rest], Line, Column, _Kind, Length, Op, Scope, Tokens) when ?is_space(hd(Rest)) -> Token = {kw_identifier, {Line, Column, nil}, Op}, tokenize(Rest, Line, Column + Length + 1, Scope, [Token | Tokens]); handle_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) -> case strip_horizontal_space(Rest) of {[$/ | _] = Remaining, Extra} -> Token = {identifier, {Line, Column, nil}, Op}, tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens]); {Remaining, Extra} -> Token = {Kind, {Line, Column, previous_was_eol(Tokens)}, Op}, tokenize(Remaining, Line, Column + Length + Extra, Scope, add_token_with_eol(Token, Tokens)) end. % ## Three Token Operators handle_dot([$., T1, T2, T3 | Rest], Line, Column, DotInfo, Scope, Tokens) when ?unary_op3(T1, T2, T3); ?comp_op3(T1, T2, T3); ?and_op3(T1, T2, T3); ?or_op3(T1, T2, T3); ?arrow_op3(T1, T2, T3); ?three_op(T1, T2, T3) -> handle_call_identifier(Rest, Line, Column, DotInfo, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); % ## Two Token Operators handle_dot([$., T1, T2 | Rest], Line, Column, DotInfo, Scope, Tokens) when ?comp_op2(T1, T2); ?rel_op2(T1, T2); ?and_op(T1, T2); ?or_op(T1, T2); ?arrow_op(T1, T2); ?in_match_op(T1, T2); ?two_op(T1, T2); ?type_op(T1, T2) -> handle_call_identifier(Rest, Line, Column, DotInfo, 2, list_to_atom([T1, T2]), Scope, Tokens); % ## Single Token Operators handle_dot([$., T | Rest], Line, Column, DotInfo, Scope, Tokens) when ?at_op(T); ?unary_op(T); ?capture_op(T); ?dual_op(T); ?mult_op(T); ?rel_op(T); ?match_op(T); ?pipe_op(T) -> handle_call_identifier(Rest, Line, Column, DotInfo, 1, list_to_atom([T]), Scope, Tokens); % ## Exception for .( as it needs to be treated specially in the parser handle_dot([$., $( | Rest], Line, Column, DotInfo, Scope, Tokens) -> TokensSoFar = add_token_with_eol({dot_call_op, DotInfo, '.'}, Tokens), tokenize([$( | Rest], Line, Column + 2, Scope, TokensSoFar); handle_dot([$., H | T] = Original, Line, Column, DotInfo, Scope, Tokens) when ?is_quote(H) -> case elixir_interpolation:extract(Line, Column + 1, Scope, true, T, H) of {NewLine, NewColumn, [Part], Rest} when is_binary(Part) -> case unsafe_to_atom(Part, Line, Scope) of {ok, Atom} -> Token = check_call_identifier(Line, Column, Atom, Rest), TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens), tokenize(Rest, NewLine, NewColumn, Scope, [Token | TokensSoFar]); {error, Reason} -> {error, Reason, Original, Tokens} end; {error, Reason} -> interpolation_error(Reason, Original, Tokens, " (for function name starting at line ~B)", [Line]) end; handle_dot([$. | Rest], Line, Column, DotInfo, Scope, Tokens) -> TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens), tokenize(Rest, Line, Column, Scope, TokensSoFar). handle_call_identifier(Rest, Line, Column, DotInfo, Length, Op, Scope, Tokens) -> Token = check_call_identifier(Line, Column, Op, Rest), TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens), tokenize(Rest, Line, Column + Length, Scope, [Token | TokensSoFar]). % ## Ambiguous unary/binary operators tokens handle_space_sensitive_tokens([Sign, NotMarker | T], Line, Column, Scope, [{Identifier, _, _} = H | Tokens]) when ?dual_op(Sign), not(?is_space(NotMarker)), NotMarker /= $(, NotMarker /= $[, NotMarker /= $<, NotMarker /= ${, %% containers NotMarker /= $%, NotMarker /= $+, NotMarker /= $-, NotMarker /= $/, NotMarker /= $>, %% operators Identifier == identifier -> Rest = [NotMarker | T], DualOpToken = {dual_op, {Line, Column, nil}, list_to_atom([Sign])}, tokenize(Rest, Line, Column + 1, Scope, [DualOpToken, setelement(1, H, op_identifier) | Tokens]); handle_space_sensitive_tokens(String, Line, Column, Scope, Tokens) -> tokenize(String, Line, Column, Scope, Tokens). %% Helpers eol(_Line, _Column, [{',', {Line, Column, Count}} | Tokens]) -> [{',', {Line, Column, Count + 1}} | Tokens]; eol(_Line, _Column, [{';', {Line, Column, Count}} | Tokens]) -> [{';', {Line, Column, Count + 1}} | Tokens]; eol(_Line, _Column, [{eol, {Line, Column, Count}} | Tokens]) -> [{eol, {Line, Column, Count + 1}} | Tokens]; eol(Line, Column, Tokens) -> [{eol, {Line, Column, 1}} | Tokens]. unsafe_to_atom(Part, Line, #elixir_tokenizer{}) when is_binary(Part) andalso size(Part) > 255; is_list(Part) andalso length(Part) > 255 -> {error, {Line, "atom length must be less than system limit: ", [$: | Part]}}; unsafe_to_atom(Binary, _Line, #elixir_tokenizer{existing_atoms_only=true}) when is_binary(Binary) -> {ok, binary_to_existing_atom(Binary, utf8)}; unsafe_to_atom(Binary, _Line, #elixir_tokenizer{}) when is_binary(Binary) -> {ok, binary_to_atom(Binary, utf8)}; unsafe_to_atom(List, _Line, #elixir_tokenizer{existing_atoms_only=true}) when is_list(List) -> {ok, list_to_existing_atom(List)}; unsafe_to_atom(List, _Line, #elixir_tokenizer{}) when is_list(List) -> {ok, list_to_atom(List)}. collect_modifiers([H | T], Buffer) when ?is_downcase(H) or ?is_upcase(H) -> collect_modifiers(T, [H | Buffer]); collect_modifiers(Rest, Buffer) -> {Rest, lists:reverse(Buffer)}. %% Heredocs extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) -> case extract_heredoc(Line, Column, T, H) of {ok, NewLine, NewColumn, Body, Rest} -> case elixir_interpolation:extract(Line + 1, 1, Scope, Interpol, Body, 0) of {error, Reason} -> {error, interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line])}; {_, _, Parts, []} -> {ok, NewLine, NewColumn, Parts, Rest} end; {error, _} = Error -> Error end. extract_heredoc(Line0, Column0, Rest0, Marker) -> case extract_heredoc_header(Rest0) of {ok, Rest1} -> %% We prepend a new line so we can transparently remove %% spaces later. This new line is removed by calling "tl" %% in the final heredoc body three lines below. case extract_heredoc_body(Line0, Column0, Marker, [$\n | Rest1], []) of {ok, Line1, Body, Rest2, Spaces} -> {ok, Line1, 1, tl(remove_heredoc_spaces(Body, Spaces)), Rest2}; {error, Reason, ErrorLine} -> Terminator = [Marker, Marker, Marker], {Message, Token} = heredoc_error_message(Reason, Line0, Terminator), {error, {ErrorLine, Message, Token}} end; error -> Message = "heredoc start must be followed by a new line after ", {error, {Line0, io_lib:format(Message, []), [Marker, Marker, Marker]}} end. heredoc_error_message(eof, Line, Terminator) -> {io_lib:format("missing terminator: ~ts (for heredoc starting at line ~B)", [Terminator, Line]), []}; heredoc_error_message(misplacedterminator, _Line, Terminator) -> {"invalid location for heredoc terminator, please escape token or move it to its own line: ", Terminator}. %% Remove spaces from heredoc based on the position of the final quotes. remove_heredoc_spaces(Body, 0) -> lists:reverse([0 | Body]); remove_heredoc_spaces(Body, Spaces) -> remove_heredoc_spaces([0 | Body], [], Spaces, Spaces). remove_heredoc_spaces([H, $\n | T], [Backtrack | Buffer], Spaces, Original) when Spaces > 0, ?is_horizontal_space(H) -> remove_heredoc_spaces([Backtrack, $\n | T], Buffer, Spaces - 1, Original); remove_heredoc_spaces([$\n=H | T], Buffer, _Spaces, Original) -> remove_heredoc_spaces(T, [H | Buffer], Original, Original); remove_heredoc_spaces([H | T], Buffer, Spaces, Original) -> remove_heredoc_spaces(T, [H | Buffer], Spaces, Original); remove_heredoc_spaces([], Buffer, _Spaces, _Original) -> Buffer. %% Extract the heredoc header. extract_heredoc_header("\r\n" ++ Rest) -> {ok, Rest}; extract_heredoc_header("\n" ++ Rest) -> {ok, Rest}; extract_heredoc_header([H | T]) when ?is_horizontal_space(H) -> extract_heredoc_header(T); extract_heredoc_header(_) -> error. %% Extract heredoc body. It returns the heredoc body (in reverse order), %% the remaining of the document and the number of spaces the heredoc %% is aligned. extract_heredoc_body(Line, _Column, Marker, Rest, Buffer) -> case extract_heredoc_line(Marker, Rest, Buffer, 0) of {ok, NewBuffer, NewRest} -> extract_heredoc_body(Line + 1, 1, Marker, NewRest, NewBuffer); {ok, NewBuffer, NewRest, Spaces} -> {ok, Line, NewBuffer, NewRest, Spaces}; {error, Reason} -> {error, Reason, Line} end. %% Extract a line from the heredoc prepending its contents to a buffer. %% Allow lazy escaping (e.g. \""") extract_heredoc_line(Marker, [$\\, $\\ | T], Buffer) -> extract_heredoc_line(Marker, T, [$\\, $\\ | Buffer]); extract_heredoc_line(Marker, [$\\, Marker | T], Buffer) -> extract_heredoc_line(Marker, T, [Marker, $\\ | Buffer]); extract_heredoc_line(Marker, [Marker, Marker, Marker | _], _) -> {error, misplacedterminator}; extract_heredoc_line(_, "\r\n" ++ Rest, Buffer) -> {ok, [$\n | Buffer], Rest}; extract_heredoc_line(_, "\n" ++ Rest, Buffer) -> {ok, [$\n | Buffer], Rest}; extract_heredoc_line(Marker, [H | T], Buffer) -> extract_heredoc_line(Marker, T, [H | Buffer]); extract_heredoc_line(_, _, _) -> {error, eof}. %% Extract each heredoc line trying to find a match according to the marker. extract_heredoc_line(Marker, [H | T], Buffer, Counter) when ?is_horizontal_space(H) -> extract_heredoc_line(Marker, T, [H | Buffer], Counter + 1); extract_heredoc_line(Marker, [Marker, Marker, Marker | T], Buffer, Counter) -> {ok, Buffer, T, Counter}; extract_heredoc_line(Marker, Rest, Buffer, _Counter) -> extract_heredoc_line(Marker, Rest, Buffer). unescape_tokens(Tokens, #elixir_tokenizer{unescape=true}) -> elixir_interpolation:unescape_tokens(Tokens); unescape_tokens(Tokens, #elixir_tokenizer{unescape=false}) -> Tokens. %% Integers and floats %% At this point, we are at least sure the first digit is a number. %% Check if we have a point followed by a number; tokenize_number([$., H | T], Acc, Length, false) when ?is_digit(H) -> tokenize_number(T, [H, $. | Acc], Length + 2, true); %% Check if we have an underscore followed by a number; tokenize_number([$_, H | T], Acc, Length, Bool) when ?is_digit(H) -> tokenize_number(T, [H, $_ | Acc], Length + 2, Bool); %% Check if we have e- followed by numbers (valid only for floats); tokenize_number([E, S, H | T], Acc, Length, true) when (E == $E) or (E == $e), ?is_digit(H), S == $+ orelse S == $- -> tokenize_number(T, [H, S, E | Acc], Length + 3, true); %% Check if we have e followed by numbers (valid only for floats); tokenize_number([E, H | T], Acc, Length, true) when (E == $E) or (E == $e), ?is_digit(H) -> tokenize_number(T, [H, E | Acc], Length + 2, true); %% Finally just numbers. tokenize_number([H | T], Acc, Length, Bool) when ?is_digit(H) -> tokenize_number(T, [H | Acc], Length + 1, Bool); %% Cast to float... tokenize_number(Rest, Acc, Length, true) -> try {Number, Original} = reverse_number(Acc, [], []), {Rest, list_to_float(Number), Original, Length} catch error:badarg -> {error, "invalid float number ", lists:reverse(Acc)} end; %% Or integer. tokenize_number(Rest, Acc, Length, false) -> {Number, Original} = reverse_number(Acc, [], []), {Rest, list_to_integer(Number), Original, Length}. tokenize_hex([H | T], Acc, Length) when ?is_hex(H) -> tokenize_hex(T, [H | Acc], Length + 1); tokenize_hex([$_, H | T], Acc, Length) when ?is_hex(H) -> tokenize_hex(T, [H, $_ | Acc], Length + 2); tokenize_hex(Rest, Acc, Length) -> {Number, Original} = reverse_number(Acc, [], []), {Rest, list_to_integer(Number, 16), [$0, $x | Original], Length}. tokenize_octal([H | T], Acc, Length) when ?is_octal(H) -> tokenize_octal(T, [H | Acc], Length + 1); tokenize_octal([$_, H | T], Acc, Length) when ?is_octal(H) -> tokenize_octal(T, [H, $_ | Acc], Length + 2); tokenize_octal(Rest, Acc, Length) -> {Number, Original} = reverse_number(Acc, [], []), {Rest, list_to_integer(Number, 8), [$0, $o | Original], Length}. tokenize_bin([H | T], Acc, Length) when ?is_bin(H) -> tokenize_bin(T, [H | Acc], Length + 1); tokenize_bin([$_, H | T], Acc, Length) when ?is_bin(H) -> tokenize_bin(T, [H, $_ | Acc], Length + 2); tokenize_bin(Rest, Acc, Length) -> {Number, Original} = reverse_number(Acc, [], []), {Rest, list_to_integer(Number, 2), [$0, $b | Original], Length}. reverse_number([$_ | T], Number, Original) -> reverse_number(T, Number, [$_ | Original]); reverse_number([H | T], Number, Original) -> reverse_number(T, [H | Number], [H | Original]); reverse_number([], Number, Original) -> {Number, Original}. %% Comments reset_eol([{eol, {Line, Column, _}} | Rest]) -> [{eol, {Line, Column, 0}} | Rest]; reset_eol(Rest) -> Rest. tokenize_comment("\r\n" ++ _ = Rest, Acc) -> {Rest, lists:reverse(Acc)}; tokenize_comment("\n" ++ _ = Rest, Acc) -> {Rest, lists:reverse(Acc)}; tokenize_comment([H | Rest], Acc) -> tokenize_comment(Rest, [H | Acc]); tokenize_comment([], Acc) -> {[], lists:reverse(Acc)}. preserve_comments(Line, Column, Tokens, Comment, Rest, Scope) -> case Scope#elixir_tokenizer.preserve_comments of Fun when is_function(Fun) -> Fun(Line, Column, Tokens, Comment, Rest); nil -> ok end. %% Identifiers tokenize([H | T]) when ?is_upcase(H) -> {Acc, Rest, Length, Special} = tokenize_continue(T, [H], 1, []), {alias, lists:reverse(Acc), Rest, Length, true, Special}; tokenize([H | T]) when ?is_downcase(H); H == $_ -> {Acc, Rest, Length, Special} = tokenize_continue(T, [H], 1, []), {identifier, lists:reverse(Acc), Rest, Length, true, Special}; tokenize(_List) -> {error, empty}. tokenize_continue([$@ | T], Acc, Length, Special) -> tokenize_continue(T, [$@ | Acc], Length + 1, [$@ | lists:delete($@, Special)]); tokenize_continue([$! | T], Acc, Length, Special) -> {[$! | Acc], T, Length + 1, [$! | Special]}; tokenize_continue([$? | T], Acc, Length, Special) -> {[$? | Acc], T, Length + 1, [$? | Special]}; tokenize_continue([H | T], Acc, Length, Special) when ?is_upcase(H); ?is_downcase(H); ?is_digit(H); H == $_ -> tokenize_continue(T, [H | Acc], Length + 1, Special); tokenize_continue(Rest, Acc, Length, Special) -> {Acc, Rest, Length, Special}. tokenize_identifier(String, Line, Scope) -> case (Scope#elixir_tokenizer.identifier_tokenizer):tokenize(String) of {Kind, Acc, Rest, Length, Ascii, Special} -> case unsafe_to_atom(Acc, Line, Scope) of {ok, Atom} -> {Kind, Atom, Rest, Length, Ascii, Special}; {error, _Reason} = Error -> Error end; {error, {not_nfc, Wrong}} -> Right = unicode:characters_to_nfc_list(Wrong), RightCodepoints = list_to_codepoint_hex(Right), WrongCodepoints = list_to_codepoint_hex(Wrong), Message = io_lib:format("Elixir expects unquoted Unicode atoms and variables to be in NFC form.\n\n" "Got:\n\n \"~ts\" (codepoints~ts)\n\n" "Expected:\n\n \"~ts\" (codepoints~ts)\n\n" "Syntax error before: ", [Wrong, WrongCodepoints, Right, RightCodepoints]), {error, {Line, Message, Wrong}}; {error, empty} -> empty end. list_to_codepoint_hex(List) -> [io_lib:format(" ~4.16.0B", [Codepoint]) || Codepoint <- List]. tokenize_alias(Rest, Line, Column, Atom, Length, Ascii, Special, Scope, Tokens) -> if not Ascii -> AtomName = atom_to_list(Atom), Invalid = hd([C || C <- AtomName, C > 127]), Reason = {Line, invalid_character_error("alias (only ascii characters are allowed)", Invalid), AtomName}, {error, Reason, AtomName ++ Rest, Tokens}; Special /= [] -> AtomName = atom_to_list(Atom), Reason = {Line, invalid_character_error("alias", hd(Special)), AtomName}, {error, Reason, AtomName ++ Rest, Tokens}; true -> AliasesToken = {alias, {Line, Column, nil}, Atom}, tokenize(Rest, Line, Column + Length, Scope, [AliasesToken | Tokens]) end. tokenize_other(Rest, Line, Column, Atom, Length, Scope, Tokens) -> case tokenize_keyword_or_identifier(Rest, Line, Column, Atom, Tokens) of {keyword, Rest, Check, T} -> handle_terminator(Rest, Line, Column + Length, Scope, Check, T); {keyword, Rest, Check, T} -> handle_terminator(Rest, Line, Column + Length, Scope, Check, T); {identifier, Rest, Token} -> tokenize(Rest, Line, Column + Length, Scope, [Token | Tokens]); {error, _, _, _} = Error -> Error end. tokenize_keyword_or_identifier(Rest, Line, Column, Atom, Tokens) -> case check_keyword(Line, Column, Atom, Tokens, Rest) of nomatch -> {identifier, Rest, check_call_identifier(Line, Column, Atom, Rest)}; {ok, [{in_op, _, in} | [{unary_op, NotInfo, 'not'} | T]]} -> {keyword, Rest, {in_op, NotInfo, 'not in'}, T}; {ok, [Check | T]} -> {keyword, Rest, Check, T}; {error, Message, Token} -> {error, {Line, Message, Token}, atom_to_list(Atom) ++ Rest, Tokens} end. %% Check if it is a call identifier (paren | bracket | do) check_call_identifier(Line, Column, Atom, [$( | _]) -> {paren_identifier, {Line, Column, nil}, Atom}; check_call_identifier(Line, Column, Atom, [$[ | _]) -> {bracket_identifier, {Line, Column, nil}, Atom}; check_call_identifier(Line, Column, Atom, _Rest) -> {identifier, {Line, Column, nil}, Atom}. add_token_with_eol({unary_op, _, _} = Left, T) -> [Left | T]; add_token_with_eol(Left, [{eol, _} | T]) -> [Left | T]; add_token_with_eol(Left, T) -> [Left | T]. previous_was_eol([{',', {_, _, Count}} | _]) when Count > 0 -> eol; previous_was_eol([{';', {_, _, Count}} | _]) when Count > 0 -> eol; previous_was_eol([{eol, {_, _, Count}} | _]) when Count > 0 -> eol; previous_was_eol(_) -> nil. %% Error handling interpolation_error(Reason, Rest, Tokens, Extension, Args) -> {error, interpolation_format(Reason, Extension, Args), Rest, Tokens}. interpolation_format({string, Line, Message, Token}, Extension, Args) -> {Line, io_lib:format("~ts" ++ Extension, [Message | Args]), Token}; interpolation_format({_, _, _} = Reason, _Extension, _Args) -> Reason. %% Terminators handle_terminator(Rest, Line, Column, Scope, Token, Tokens) -> case handle_terminator(Token, Scope) of {error, Reason} -> {error, Reason, atom_to_list(element(1, Token)) ++ Rest, Tokens}; New -> tokenize(Rest, Line, Column, New, [Token | Tokens]) end. handle_terminator(_, #elixir_tokenizer{check_terminators=false} = Scope) -> Scope; handle_terminator(Token, #elixir_tokenizer{terminators=Terminators} = Scope) -> case check_terminator(Token, Terminators) of {error, _} = Error -> Error; New -> Scope#elixir_tokenizer{terminators=New} end. check_terminator({S, _} = New, Terminators) when S == 'fn'; S == 'do'; S == '('; S == '['; S == '{'; S == '<<' -> [New | Terminators]; check_terminator({E, _}, [{S, _} | Terminators]) when S == 'do', E == 'end'; S == 'fn', E == 'end'; S == '(', E == ')'; S == '[', E == ']'; S == '{', E == '}'; S == '<<', E == '>>' -> Terminators; check_terminator({E, {Line, _, _}}, [{Start, {StartLine, _, _}} | _]) when E == 'end'; E == ')'; E == ']'; E == '}'; E == '>>' -> End = terminator(Start), MessagePrefix = io_lib:format("\"~ts\" is missing terminator \"~ts\". unexpected token: \"", [Start, End]), MessageSuffix = io_lib:format("\" at line ~B", [Line]), {error, {StartLine, {MessagePrefix, MessageSuffix}, [atom_to_list(E)]}}; %% We do not prematurely raise on end because it is likelly the %% whole code is inside do/end, so the parser raises better errors. check_terminator({E, Line}, []) when E == ')'; E == ']'; E == '}'; E == '>>' -> {error, {Line, "unexpected token: ", atom_to_list(E)}}; check_terminator(_, Terminators) -> Terminators. string_type($") -> bin_string; string_type($') -> list_string. heredoc_type($") -> bin_heredoc; heredoc_type($') -> list_heredoc. sigil_terminator($() -> $); sigil_terminator($[) -> $]; sigil_terminator(${) -> $}; sigil_terminator($<) -> $>; sigil_terminator(O) -> O. terminator('fn') -> 'end'; terminator('do') -> 'end'; terminator('(') -> ')'; terminator('[') -> ']'; terminator('{') -> '}'; terminator('<<') -> '>>'. %% Keywords checking check_keyword(_Line, _Column, _Atom, [{'.', _} | _], _Rest) -> nomatch; check_keyword(DoLine, DoColumn, do, [{Identifier, {Line, Column, Meta}, Atom} | T], _Rest) when Identifier == identifier -> {ok, add_token_with_eol({do, {DoLine, DoColumn, nil}}, [{do_identifier, {Line, Column, Meta}, Atom} | T])}; check_keyword(_Line, _Column, do, [{'fn', _} | _], _Rest) -> {error, do_with_fn_error("unexpected token \"do\""), "do"}; check_keyword(Line, Column, do, Tokens, _Rest) -> case do_keyword_valid(Tokens) of true -> {ok, add_token_with_eol({do, {Line, Column, nil}}, Tokens)}; false -> {error, invalid_do_error("unexpected token \"do\""), "do"} end; check_keyword(Line, Column, Atom, Tokens, Rest) -> case keyword(Atom) of false -> nomatch; token -> {ok, [{Atom, {Line, Column, nil}} | Tokens]}; block -> {ok, [{block_identifier, {Line, Column, nil}, Atom} | Tokens]}; Kind -> case strip_horizontal_space(Rest) of {[$/ | _], _} -> {ok, [{identifier, {Line, Column, nil}, Atom} | Tokens]}; _ -> {ok, add_token_with_eol({Kind, {Line, Column, previous_was_eol(Tokens)}, Atom}, Tokens)} end end. %% Fail early on invalid do syntax. For example, after %% most keywords, after comma and so on. do_keyword_valid([{Atom, _} | _]) -> case Atom of ',' -> false; ';' -> false; 'end' -> true; nil -> true; true -> true; false -> true; _ -> keyword(Atom) == false end; do_keyword_valid(_) -> true. % Regular keywords keyword('fn') -> token; keyword('end') -> token; keyword('true') -> token; keyword('false') -> token; keyword('nil') -> token; % Operators keywords keyword('not') -> unary_op; keyword('and') -> and_op; keyword('or') -> or_op; keyword('when') -> when_op; keyword('in') -> in_op; % Block keywords keyword('after') -> block; keyword('else') -> block; keyword('rescue') -> block; keyword('catch') -> block; keyword(_) -> false. invalid_character_error(What, Char) -> io_lib:format("invalid character \"~ts\" (codepoint U+~4.16.0B) in ~ts: ", [[Char], Char, What]). invalid_do_error(Prefix) -> Prefix ++ ". In case you wanted to write a \"do\" expression, " "you must either separate the keyword argument with comma or use do-blocks. " "For example, the following construct:\n\n" " if some_condition? do\n" " :this\n" " else\n" " :that\n" " end\n\n" "is syntactic sugar for the Elixir construct:\n\n" " if(some_condition?, do: :this, else: :that)\n\n" "where \"some_condition?\" is the first argument and the second argument is a keyword list.\n\n" "Syntax error before: ". do_with_fn_error(Prefix) -> Prefix ++ ". Anonymous functions are written as:\n\n" " fn pattern -> expression end\n\n" "Syntax error before: ".