Skip to content

Commit b42745c

Browse files
committed
feat: add yecc backend
1 parent 5866352 commit b42745c

File tree

9 files changed

+263
-97
lines changed

9 files changed

+263
-97
lines changed

bench/decode.exs

+2-11
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,11 @@ medium = File.read!("./bench/data.json")
1010
Benchee.run(
1111
%{
1212
"jason" => fn input -> Jason.decode!(input) end,
13-
"json5" => fn input -> Json5.decode!(input) end
13+
"json5 combine" => fn input -> Json5.decode!(input, backend: Json5.Decode.Backend.Combine) end,
14+
"json5 yecc" => fn input -> Json5.decode!(input, backend: Json5.Decode.Backend.Yecc) end,
1415
},
1516
inputs: %{
1617
"Small" => small,
1718
"Medium" => medium
1819
}
1920
)
20-
21-
22-
# Benchee.run(
23-
# %{
24-
# "json5" => fn input -> Json5.decode!(input) end
25-
# },
26-
# inputs: %{
27-
# "Medium" => medium
28-
# }
29-
# )

lib/json5/decode.ex

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ defmodule Json5.Decode do
44
"""
55

66
def parse(input, config \\ %{}) do
7-
backend().parse(input, config)
7+
backend(config).parse(input, config)
88
end
99

10-
defp backend do
10+
defp backend(%{backend: backend}) do
11+
backend
12+
end
13+
14+
defp backend(_) do
1115
Json5.Decode.Backend.Combine
1216
end
1317
end

lib/json5/decode/backend/yecc.ex

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule Json5.Decode.Backend.Yecc do
2+
@moduledoc """
3+
Decode Json5 string to Elixir term
4+
5+
Does not support unicode map keys
6+
"""
7+
8+
import Json5.ECMA
9+
10+
alias Json5.Error
11+
12+
def parse(input, config \\ %{}) do
13+
with {:ok, tokens, _} <-
14+
input
15+
|> String.to_charlist()
16+
|> :lexer.string(),
17+
{:ok, ast} <- :parser.parse(tokens) do
18+
{:ok, to_term(ast, config)}
19+
end
20+
rescue
21+
e in Error -> {:error, e}
22+
end
23+
24+
defp to_term({:string, _, charlist}, _) do
25+
charlist
26+
|> :string.replace([92, 13, 10], [])
27+
|> :string.replace([92, 10], [])
28+
|> :erlang.iolist_to_binary()
29+
end
30+
31+
defp to_term({:key, _, charlist}, config) do
32+
key = List.to_string(charlist)
33+
34+
if is_reserved_word(key),
35+
do: raise(Error, %{type: :reserved_key, input: key})
36+
37+
do_key_term(key, config)
38+
end
39+
40+
defp to_term({:map, _, key_value_list}, config) do
41+
Map.new(key_value_list, fn {key, value} ->
42+
{to_term(key, config), to_term(value, config)}
43+
end)
44+
end
45+
46+
defp to_term({:list, _, list}, config) do
47+
Enum.map(list, &to_term(&1, config))
48+
end
49+
50+
defp to_term({:null, _, nil}, _), do: nil
51+
defp to_term({:boolean, _, boolean}, _), do: boolean
52+
53+
defp to_term({:hex_number, _, integer}, _) do
54+
Decimal.new(integer)
55+
end
56+
57+
defp to_term({:number, _, charlist}, _) do
58+
charlist
59+
|> List.to_string()
60+
|> Decimal.new()
61+
end
62+
63+
def do_key_term(key, %{object_key_existing_atom: true}) do
64+
String.to_existing_atom(key)
65+
end
66+
67+
def do_key_term(key, %{object_key_function: object_key_function}) do
68+
object_key_function.(key)
69+
end
70+
71+
def do_key_term(key, _), do: key
72+
end

lib/json5/encode.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ defmodule Json5.Encode do
66
require Json5.ECMA
77

88
alias Json5.Encode.Array
9-
alias Json5.Encode.Error
109
alias Json5.Encode.Object
10+
alias Json5.Error
1111

1212
defguardp is_to_string(input)
1313
when input in [true, false] or is_float(input) or is_integer(input) or

lib/json5/encode/error.ex lib/json5/error.ex

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
defmodule Json5.Encode.Error do
1+
defmodule Json5.Error do
22
defexception [:type, :input]
33

44
@impl true
55
def exception(%{type: type, input: input}) do
66
case type do
77
:invalid_input -> :ok
8+
:reserved_key -> :ok
89
_ -> raise ArgumentError
910
end
1011

1112
%__MODULE__{type: type, input: input}
1213
end
1314

1415
@impl true
15-
def message(%__MODULE__{type: type}) do
16+
def message(%__MODULE__{type: type, input: input}) do
1617
case type do
1718
:invalid_input -> "unable to format input"
19+
:reserved_key -> "found a reserved word, '#{input}'"
1820
_ -> "something went wrong"
1921
end
2022
end

src/lexer.xrl

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Definitions.
2+
Digit = [0-9]
3+
HexDigit = [0-9a-fA-F]
4+
Float = (\+|-)?{Digit}+\.?{Digit}+?((E|e)(\+|-)?{Digit}+)?
5+
FloatNoLeadingZero = (\+|-)?\.{Digit}+((E|e)(\+|-)?{Digit}+)?
6+
HexDigits = 0(x|X){HexDigit}+
7+
DoubleQuoteString = "(\\\^.|\\.|[^\"])*"
8+
SingleQuoteString = '(\\\^.|\\.|[^\'])*'
9+
Key = [a-zA-Z\_\$](\\\^.|\\.|[a-zA-Z0-9\_\$])*
10+
11+
Rules.
12+
13+
{Digit}+ : {token,{number,TokenLine,TokenChars}}.
14+
15+
{Float} : {token, {number, TokenLine, TokenChars}}.
16+
{FloatNoLeadingZero} : {token, {number, TokenLine, TokenChars}}.
17+
{HexDigits} : {token, {hex_number, TokenLine, parse_hex(TokenChars)}}.
18+
19+
null : {token, {null, TokenLine, nil}}.
20+
true : {token, {boolean, TokenLine, true}}.
21+
false : {token, {boolean, TokenLine, false}}.
22+
23+
{DoubleQuoteString} : {token, {string, TokenLine, strip_quotes(TokenChars)}}.
24+
{SingleQuoteString} : {token, {string, TokenLine, strip_quotes(TokenChars)}}.
25+
{Key} : {token, {key, TokenLine, TokenChars}}.
26+
27+
\[ : {token, {open_list, TokenLine}}.
28+
\] : {token, {close_list, TokenLine}}.
29+
\{ : {token, {open_map, TokenLine}}.
30+
\} : {token, {close_map, TokenLine}}.
31+
\, : {token, {sep, TokenLine}}.
32+
\: : {token, {key_value_sep, TokenLine}}.
33+
34+
%% white space
35+
[\s\n\r\t]+ : skip_token.
36+
%% line comment
37+
//[^\n]* : skip_token.
38+
%% multiline comment
39+
/\*[^(*/)]*\*/ : skip_token.
40+
41+
42+
43+
Erlang code.
44+
strip_quotes(Str) ->
45+
tl(lists:droplast(Str)).
46+
47+
parse_hex(Str) ->
48+
list_to_integer(tl(tl(Str)), 16).

src/parser.yrl

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Nonterminals term factor seq_items key_value_items key_index key_value.
2+
Terminals number hex_number open_list close_list open_map close_map null boolean string key sep key_value_sep.
3+
Rootsymbol term.
4+
5+
term -> factor : '$1'.
6+
7+
factor -> open_list close_list : {list, line('$1'), []}.
8+
factor -> open_list seq_items close_list : {list, line('$1'), '$2'}.
9+
factor -> open_map close_map : {map, line('$1'), []}.
10+
factor -> open_map key_value_items close_map : {map, line('$1'), '$2'}.
11+
12+
factor -> number : '$1'.
13+
factor -> string : '$1'.
14+
factor -> null : '$1'.
15+
factor -> boolean : '$1'.
16+
factor -> hex_number: '$1'.
17+
18+
seq_items -> term : ['$1'].
19+
seq_items -> term sep : ['$1'].
20+
seq_items -> term sep seq_items : ['$1'|'$3'].
21+
key_value_items -> key_value : ['$1'].
22+
key_value_items -> key_value sep : ['$1'].
23+
key_value_items -> key_value sep key_value_items : ['$1'|'$3'].
24+
key_value -> key_index key_value_sep term : {'$1', '$3'}.
25+
26+
key_index -> key : '$1'.
27+
key_index -> string : '$1'.
28+
29+
Erlang code.
30+
31+
line(T) when is_tuple(T) -> element(2, T);
32+
line([H|_T]) -> element(2, H);
33+
line(T) -> ct:print("WAT ~p", [T]).

0 commit comments

Comments
 (0)