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.
This commit is contained in:
Jarvis Carroll 2026-01-27 06:42:55 +00:00
parent 4f2a3c6c6f
commit 2bf384ca82
2 changed files with 40 additions and 23 deletions

View File

@ -22,8 +22,6 @@
fate_to_erlang/2, fate_to_erlang/2,
erlang_args_to_fate/2, erlang_args_to_fate/2,
get_function_signature/2]). get_function_signature/2]).
% Internal stuff that is useful for writing AACI unit tests.
-export([aaci_from_string/1, annotate_type/2]).
%%% Types %%% Types

View File

@ -438,12 +438,33 @@ check_sophia_to_fate(Type, Sophia, Fate) ->
erlang:error({to_fate_failed, Fate, FateActual}) erlang:error({to_fate_failed, Fate, FateActual})
end. 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 % 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. % syntax, and to get an AACI object to pass to the parser.
Source = "contract C = entrypoint f() = " ++ Sophia, Source = "contract C = entrypoint f() = " ++ Sophia,
{ok, AACI} = hz_aaci:aaci_from_string(Source), {Code, Type} = compile_entrypoint_code_and_type(Source, "f"),
{ok, {_, Type}} = hz_aaci:get_function_signature(AACI, "f"), Fate = extract_return_value(Code),
% Also check that the FATE term is valid, by running it through gmb. % Also check that the FATE term is valid, by running it through gmb.
gmb_fate_encoding:serialize(Fate), gmb_fate_encoding:serialize(Fate),
@ -453,11 +474,11 @@ check_parser(Sophia, Fate) ->
% Also check that it can be parsed without type information. % Also check that it can be parsed without type information.
check_sophia_to_fate(unknown_type(), Sophia, Fate). 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. % Compile the type definitions alongside the usual literal expression.
Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia, Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia,
{ok, AACI} = hz_aaci:aaci_from_string(Source), {Code, Type} = compile_entrypoint_code_and_type(Source, "f"),
{ok, {_, Type}} = hz_aaci:get_function_signature(AACI, "f"), Fate = extract_return_value(Code),
% Check the FATE term as usual. % Check the FATE term as usual.
gmb_fate_encoding:serialize(Fate), gmb_fate_encoding:serialize(Fate),
@ -467,40 +488,38 @@ check_parser_with_typedef(Typedef, Sophia, Fate) ->
check_sophia_to_fate(Type, Sophia, Fate). check_sophia_to_fate(Type, Sophia, Fate).
int_test() -> int_test() ->
check_parser("123", 123). check_parser("123").
list_test() -> list_test() ->
check_parser("[1, 2, 3]", [1, 2, 3]). check_parser("[1, 2, 3]").
list_of_lists_test() -> list_of_lists_test() ->
check_parser("[[], [1], [2, 3]]", [[], [1], [2, 3]]). check_parser("[[], [1], [2, 3]]").
tuple_test() -> 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() -> maps_test() ->
check_parser("{[1] = 2, [3] = 4}", #{1 => 2, 3 => 4}). check_parser("{[1] = 2, [3] = 4}").
records_test() -> records_test() ->
TypeDef = "record pair = {x: int, y: int}", TypeDef = "record pair = {x: int, y: int}",
Sophia = "{x = 1, y = 2}", Sophia = "{x = 1, y = 2}",
Fate = {tuple, {1, 2}}, check_parser_with_typedef(TypeDef, Sophia),
check_parser_with_typedef(TypeDef, Sophia, Fate),
% The above won't run an untyped parse on the expression, but we can. It % The above won't run an untyped parse on the expression, but we can. It
% will error, though. % will error, though.
{error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia). {error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia).
variant_test() -> variant_test() ->
TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)", 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, {}}), check_parser_with_typedef(TypeDef, "Zero"),
TestFn("One(0)", {variant, [0, 1, 2], 1, {0}}), check_parser_with_typedef(TypeDef, "One(0)"),
TestFn("Two(0, 1)", {variant, [0, 1, 2], 2, {0, 1}}), check_parser_with_typedef(TypeDef, "Two(0, 1)"),
TestFn("Two([], [1, 2, 3])", {variant, [0, 1, 2], 2, {[], [1, 2, 3]}}), check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])"),
{error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"),
ok. ok.