From 2bf384ca826fba47f689764da22bb1c875c9462d Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Tue, 27 Jan 2026 06:42:55 +0000 Subject: [PATCH] Infer correct values for tests automatically Now tests compare the literal parser against the output of the compiler. The little example contracts we are compiling for the AACI already had the FATE value in them, in the form of the instruction {'RETURNR', {immediate, FateValue}} so we just extract that and use it for the tests. --- src/hz_aaci.erl | 2 -- src/hz_sophia.erl | 61 +++++++++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index dfd158c..f9da33f 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -22,8 +22,6 @@ fate_to_erlang/2, erlang_args_to_fate/2, get_function_signature/2]). -% Internal stuff that is useful for writing AACI unit tests. --export([aaci_from_string/1, annotate_type/2]). %%% Types diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl index d63d1be..a158ba8 100644 --- a/src/hz_sophia.erl +++ b/src/hz_sophia.erl @@ -438,12 +438,33 @@ check_sophia_to_fate(Type, Sophia, Fate) -> erlang:error({to_fate_failed, Fate, FateActual}) end. -check_parser(Sophia, Fate) -> +compile_entrypoint_code_and_type(Source, Entrypoint) -> + {ok, #{fate_code := FateCode, aci := ACI}} = so_compiler:from_string(Source, [{aci, json}]), + + % Find the fcode for the correct entrypoint. + {fcode, Bodies, NamesMap, _} = FateCode, + Names = maps:to_list(NamesMap), + Name = unicode:characters_to_binary(Entrypoint), + {Hash, Name} = lists:keyfind(Name, 2, Names), + {_, _, Code} = maps:get(Hash, Bodies), + + % Generate the AACI, and get the AACI type info for the correct entrypoint. + AACI = hz_aaci:prepare_aaci(ACI), + {ok, {_, Type}} = hz_aaci:get_function_signature(AACI, "f"), + + {Code, Type}. + +extract_return_value(#{0 := [{'RETURNR', {immediate, FATE}}]}) -> + FATE; +extract_return_value(Code) -> + erlang:exit({invalid_literal_fcode, Code}). + +check_parser(Sophia) -> % 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"), + {Code, Type} = compile_entrypoint_code_and_type(Source, "f"), + Fate = extract_return_value(Code), % Also check that the FATE term is valid, by running it through gmb. gmb_fate_encoding:serialize(Fate), @@ -453,11 +474,11 @@ check_parser(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) -> +check_parser_with_typedef(Typedef, Sophia) -> % 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"), + {Code, Type} = compile_entrypoint_code_and_type(Source, "f"), + Fate = extract_return_value(Code), % Check the FATE term as usual. gmb_fate_encoding:serialize(Fate), @@ -467,40 +488,38 @@ check_parser_with_typedef(Typedef, Sophia, Fate) -> check_sophia_to_fate(Type, Sophia, Fate). int_test() -> - check_parser("123", 123). + check_parser("123"). list_test() -> - check_parser("[1, 2, 3]", [1, 2, 3]). + check_parser("[1, 2, 3]"). list_of_lists_test() -> - check_parser("[[], [1], [2, 3]]", [[], [1], [2, 3]]). + check_parser("[[], [1], [2, 3]]"). tuple_test() -> - check_parser("(1, [2, 3], (4, 5))", {tuple, {1, [2, 3], {tuple, {4, 5}}}}). + check_parser("(1, [2, 3], (4, 5))"). maps_test() -> - check_parser("{[1] = 2, [3] = 4}", #{1 => 2, 3 => 4}). + check_parser("{[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), + check_parser_with_typedef(TypeDef, Sophia), % 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). variant_test() -> TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)", - TestFn = fun(Sophia, Fate) -> - check_parser_with_typedef(TypeDef, Sophia, Fate), - {error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), Sophia) - end, - TestFn("Zero", {variant, [0, 1, 2], 0, {}}), - TestFn("One(0)", {variant, [0, 1, 2], 1, {0}}), - TestFn("Two(0, 1)", {variant, [0, 1, 2], 2, {0, 1}}), - TestFn("Two([], [1, 2, 3])", {variant, [0, 1, 2], 2, {[], [1, 2, 3]}}), + check_parser_with_typedef(TypeDef, "Zero"), + check_parser_with_typedef(TypeDef, "One(0)"), + check_parser_with_typedef(TypeDef, "Two(0, 1)"), + check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])"), + + {error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"), + ok.