defmodule Lexer do @spec scan(binary()) :: list(Token.t()) | {:error, String.t()} def scan(input) do tokenize(input, [], 1, 1) end defp tokenize(<<>>, tokens, line, row) do Enum.reverse([Token.new(:EOF, "", line, row) | tokens]) end defp tokenize(input, tokens, line, row) do case input do # String literal <> -> read_string(rest, tokens, line, row, row + 1) # number literal <> when char in ?0..?9 or char == ?- -> read_number(rest, tokens, line, row, row + 1, false, <>) # literals true/false/null <> -> case rest do <> -> tokenize(next, [Token.new(:TRUE, true, line, row) | tokens], line, row + 4) _ -> {:error, "Unexpected token at line #{line} row #{row}"} end <> -> case rest do <> -> tokenize(next, [Token.new(:FALSE, false, line, row) | tokens], line, row + 5) _ -> {:error, "Unexpected token at line #{line} row #{row}"} end <> -> case rest do <> -> tokenize(next, [Token.new(:NULL, nil, line, row) | tokens], line, row + 4) any -> {:error, "Unexpected token #{any} at line #{line} row #{row}"} end # array <> -> tokenize(rest, [Token.new(:LBRACKET, "[", line, row) | tokens], line, row + 1) <> -> tokenize(rest, [Token.new(:RBRACKET, "]", line, row) | tokens], line, row + 1) # object <> -> tokenize(rest, [Token.new(:LBRACE, "{", line, row) | tokens], line, row + 1) <> -> tokenize(rest, [Token.new(:RBRACE, "}", line, row) | tokens], line, row + 1) <> -> tokenize(rest, [Token.new(:COLON, ":", line, row) | tokens], line, row + 1) <> -> tokenize(rest, [Token.new(:COMMA, ",", line, row) | tokens], line, row + 1) # whitespaces <> when char in [?\s, ?\t] -> tokenize(rest, tokens, line, row + 1) <> -> tokenize(rest, tokens, line + 1, 1) <> -> tokenize(rest, tokens, line + 1, 1) <> -> tokenize(rest, tokens, line + 1, 1) any -> {:error, "Unexpected token #{inspect(any)} at line #{line}, row #{row + 1}"} end end defp read_number(data, tokens, line, start, row, float, acc) do case data do <> when char in ?0..?9 -> read_number(rest, tokens, line, start, row + 1, float, acc <> <>) <> when char == ?. -> read_number(rest, tokens, line, start, row + 1, true, acc <> <>) <> when char in [?e, ?E] -> case rest do <> when next_char in [?-, ?+] -> read_number( rest2, tokens, line, start, row + 2, true, acc <> <> <> <> ) <> when next_char in ?0..?9 -> read_number( rest2, tokens, line, start, row + 2, true, acc <> <> <> <> ) _ -> {:error, "Unexpected token after #{<>}, expect - or + or digit at line #{line} row #{row}"} end _ -> case float do true -> case to_number(acc, true) do {:ok, number} -> tokenize(data, [Token.new(:NUMBER, number, line, start) | tokens], line, row) {:error, msg} -> {:error, msg <> "#{line} row #{row}"} end false -> case to_number(acc, false) do {:ok, number} -> tokenize(data, [Token.new(:NUMBER, number, line, start) | tokens], line, row) {:error, msg} -> {:error, msg <> "#{line} row #{row}"} end end end end defp read_string(data, tokens, line, start, row, acc \\ "") do case data do <> -> tokenize(rest, [Token.new(:STRING, acc, line, start) | tokens], line, row + 1) <> -> read_string(rest, tokens, line, start, row + 1, acc <> <>) <<>> -> {:error, "Unexpected token at line #{line} row #{row}, expect \""} end end defp to_number(val, float) do case float do true -> try do num = String.to_float(val) {:ok, num} rescue ArgumentError -> {:error, "Invalid number format: #{val} at line "} end _ -> try do num = String.to_integer(val) {:ok, num} rescue ArgumentError -> {:error, "Invalid number format: #{val} at line "} end end end end