diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl index 927fe3c..c9f106c 100644 --- a/src/hz_sophia.erl +++ b/src/hz_sophia.erl @@ -159,7 +159,7 @@ parse_list_loop3(Inner, Tk, String, Row, Start, Acc) -> parse_record_or_map({_, _, {map, [KeyType, ValueType]}}, Tk, String, _, _) -> parse_map(KeyType, ValueType, Tk, String, #{}); parse_record_or_map({_, _, {record, Fields}}, Tk, String, _, _) -> - parse_record(Fields, Tk, String); + parse_record(Fields, Tk, String, #{}); parse_record_or_map({_, _, unknown_type}, Tk, String, _, _) -> case next_token(Tk, String) of {ok, {{character, "}", _, _, _}, NewTk, NewString}} -> @@ -174,8 +174,83 @@ parse_record_or_map({_, _, unknown_type}, Tk, String, _, _) -> parse_record_or_map({O, N, _}, _, _, Row, Start) -> {error, {wrong_type, O, N, map, Row, Start, Start}}. -parse_record(Fields, Tk, String) -> - {error, not_yet_implemented}. +parse_record(Fields, Tk, String, Acc) -> + case next_token(Tk, String) of + {ok, {{alphanum, Ident, Row, Start, End}, NewTk, NewString}} -> + parse_record2(Fields, NewTk, NewString, Acc, Ident, Row, Start, End); + {ok, {{character, "}", Row, Start, End}, NewTk, NewString}} -> + parse_record_end(Fields, NewTk, NewString, Acc, Row, Start, End); + {ok, {{_, S, Row, Start, End}, _, _}} -> + {error, {unexpected_token, S, Row, Start, End}}; + {error, Reason} -> + {error, Reason} + end. + +parse_record2(Fields, Tk, String, Acc, Ident, Row, Start, End) -> + case lists:keyfind(Ident, 1, Fields) of + {_, Type} -> + parse_record3(Fields, Tk, String, Acc, Ident, Row, Start, End, Type); + false -> + {error, {invalid_field, Ident, Row, Start, End}} + end. + +parse_record3(Fields, Tk, String, Acc, Ident, Row, Start, End, Type) -> + case maps:is_key(Ident, Acc) of + false -> + parse_record4(Fields, Tk, String, Acc, Ident, Type); + true -> + {error, {field_already_present, Ident, Row, Start, End}} + end. + +parse_record4(Fields, Tk, String, Acc, Ident, Type) -> + case expect_tokens(["="], Tk, String) of + {ok, {NewTk, NewString}} -> + parse_record5(Fields, NewTk, NewString, Acc, Ident, Type); + {error, Reason} -> + {error, Reason} + end. + +parse_record5(Fields, Tk, String, Acc, Ident, Type) -> + case parse_expression(Type, Tk, String) of + {ok, {Result, NewTk, NewString}} -> + NewAcc = maps:put(Ident, Result, Acc), + parse_record6(Fields, NewTk, NewString, NewAcc); + {error, Reason} -> + wrap_error(Reason, {record_field, Ident}) + end. + +parse_record6(Fields, Tk, String, Acc) -> + case next_token(Tk, String) of + {ok, {{character, ",", _, _, _}, NewTk, NewString}} -> + parse_record(Fields, NewTk, NewString, Acc); + {ok, {{character, "}", Row, Start, End}, NewTk, NewString}} -> + parse_record_end(Fields, NewTk, NewString, Acc, Row, Start, End); + {ok, {{_, S, Row, Start, End}, _, _}} -> + {error, {unexpected_token, S, Row, Start, End}}; + {error, Reason} -> + {error, Reason} + end. + +parse_record_end(Fields, Tk, String, FieldValues, Row, Start, End) -> + case parse_record_final_loop(Fields, FieldValues, []) of + {ok, Result} -> + {ok, {Result, Tk, String}}; + {error, {missing_field, Name}} -> + {error, {missing_field, Name, Row, Start, End}} + end. + +parse_record_final_loop([{Name, _} | Rest], FieldValues, Acc) -> + case maps:find(Name, FieldValues) of + {ok, Value} -> + parse_record_final_loop(Rest, FieldValues, [Value | Acc]); + error -> + {error, {missing_field, Name}} + end; +parse_record_final_loop([], _, FieldsReverse) -> + Fields = lists:reverse(FieldsReverse), + Tuple = list_to_tuple(Fields), + {ok, {tuple, Tuple}}. + %%% Map Parsing @@ -238,20 +313,33 @@ check_sophia_to_fate(Type, Sophia, Fate) -> erlang:error({to_fate_failed, Fate, FateActual}) end. -check_parser(Type, Sophia, Fate) -> - check_sophia_to_fate(Type, Sophia, Fate), - check_sophia_to_fate(unknown_type(), Sophia, Fate), - - % Finally, check that the FATE result is something that gmb understands. - gmb_fate_encoding:serialize(Fate), - - ok. - check_parser(Sophia, Fate) -> + % Compile the literal using the compiler, to check that it is valid Sophia + % syntax, and to get an AACI object to pass to the parser. Source = "contract C = entrypoint f() = " ++ Sophia, {ok, AACI} = hz_aaci:aaci_from_string(Source), {ok, {_, Type}} = hz_aaci:get_function_signature(AACI, "f"), - check_parser(Type, Sophia, Fate). + + % Also check that the FATE term is valid, by running it through gmb. + gmb_fate_encoding:serialize(Fate), + + % Now check that our parser produces that output. + check_sophia_to_fate(Type, Sophia, Fate), + % Also check that it can be parsed without type information. + check_sophia_to_fate(unknown_type(), Sophia, Fate). + +check_parser_with_typedef(Typedef, Sophia, Fate) -> + % Compile the type definitions alongside the usual literal expression. + Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia, + {ok, AACI} = hz_aaci:aaci_from_string(Source), + {ok, {_, Type}} = hz_aaci:get_function_signature(AACI, "f"), + + % Check the FATE term as usual. + gmb_fate_encoding:serialize(Fate), + + % Do a typed parse, as usual, but there are probably record/variant + % definitions in the AACI, so untyped parses probably don't work. + check_sophia_to_fate(Type, Sophia, Fate). int_test() -> check_parser("123", 123). @@ -265,3 +353,12 @@ list_of_lists_test() -> maps_test() -> check_parser("{[1] = 2, [3] = 4}", #{1 => 2, 3 => 4}). +records_test() -> + TypeDef = "record pair = {x: int, y: int}", + Sophia = "{x = 1, y = 2}", + Fate = {tuple, {1, 2}}, + check_parser_with_typedef(TypeDef, Sophia, Fate), + % The above won't run an untyped parse on the expression, but we can. It + % will error, though. + {error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia). +