split coerce/3 into two functions

Also renamed coerce_bindings to erlang_args_to_fate, to match.
This commit is contained in:
Jarvis Carroll 2026-01-09 04:39:58 +00:00
parent d2163c1ff8
commit 3da9bd570b
2 changed files with 204 additions and 165 deletions

View File

@ -716,7 +716,7 @@ decode_bytearray_fate(EncodedStr) ->
decode_bytearray(Type, EncodedStr) -> decode_bytearray(Type, EncodedStr) ->
case decode_bytearray_fate(EncodedStr) of case decode_bytearray_fate(EncodedStr) of
{ok, none} -> {ok, none}; {ok, none} -> {ok, none};
{ok, Object} -> hz_aaci:coerce(Type, Object, from_fate); {ok, Object} -> hz_aaci:fate_to_erlang(Type, Object);
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. end.
@ -1518,7 +1518,7 @@ encode_call_data({aaci, Label}, Fun, Args) ->
end. end.
encode_call_data2(ArgDef, Fun, Args) -> encode_call_data2(ArgDef, Fun, Args) ->
case hz_aaci:coerce_bindings(ArgDef, Args, to_fate) of case hz_aaci:erlang_args_to_fate(ArgDef, Args) of
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced);
Errors -> Errors Errors -> Errors
end. end.

View File

@ -18,8 +18,9 @@
% Contract call and serialization interface functions % Contract call and serialization interface functions
-export([prepare_contract/1, -export([prepare_contract/1,
prepare_aaci/1, prepare_aaci/1,
coerce/3, erlang_to_fate/2,
coerce_bindings/3, fate_to_erlang/2,
erlang_args_to_fate/2,
aaci_get_function_signature/2]). aaci_get_function_signature/2]).
%%% Types %%% Types
@ -441,97 +442,51 @@ substitute_opaque_types(Bindings, Types) ->
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end, Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types). lists:map(Each, Types).
%%% Coerce %%% Erlang to FATE
coerce_bindings(VarTypes, Terms, Direction) -> erlang_args_to_fate(VarTypes, Terms) ->
DefLength = length(VarTypes), DefLength = length(VarTypes),
ArgLength = length(Terms), ArgLength = length(Terms),
if if
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg); DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg);
DefLength > ArgLength -> {error, too_few_args}; DefLength > ArgLength -> {error, too_few_args};
DefLength < ArgLength -> {error, too_many_args} DefLength < ArgLength -> {error, too_many_args}
end. end.
coerce_zipped_bindings(Bindings, Direction, Tag) -> erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
coerce_zipped_bindings(Bindings, Direction, Tag, [], []).
coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) ->
{{ArgName, Type}, Term} = Next,
case coerce(Type, Term, Direction) of
{ok, NewTerm} ->
coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({Tag, ArgName}, Errors),
coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken])
end;
coerce_zipped_bindings([], _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_zipped_bindings([], _, _, _, Broken) ->
{error, combine_errors(Broken)}.
wrap_errors(Location, Errors) ->
F = fun({Error, Path}) ->
{Error, [Location | Path]}
end,
lists:map(F, Errors).
combine_errors(Broken) ->
F = fun(NextErrors, Acc) ->
NextErrors ++ Acc
end,
lists:foldl(F, [], Broken).
coerce({_, _, integer}, S, _) when is_integer(S) ->
{ok, S}; {ok, S};
coerce({O, N, integer}, S, to_fate) when is_list(S) -> erlang_to_fate({O, N, integer}, S) when is_list(S) ->
try try
Val = list_to_integer(S), Val = list_to_integer(S),
{ok, Val} {ok, Val}
catch catch
error:badarg -> single_error({invalid, O, N, S}) error:badarg -> single_error({invalid, O, N, S})
end; end;
coerce({O, N, address}, S, to_fate) -> erlang_to_fate({O, N, address}, S) ->
coerce_chain_object(O, N, address, account_pubkey, S); coerce_chain_object(O, N, address, account_pubkey, S);
coerce({_, _, address}, {address, Bin}, from_fate) -> erlang_to_fate({O, N, contract}, S) ->
Address = gmser_api_encoder:encode(account_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
coerce({O, N, contract}, S, to_fate) ->
coerce_chain_object(O, N, contract, contract_pubkey, S); coerce_chain_object(O, N, contract, contract_pubkey, S);
coerce({_, _, contract}, {contract, Bin}, from_fate) -> erlang_to_fate({_, _, signature}, S) when is_binary(S) andalso (byte_size(S) =:= 64) ->
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) ->
% Usually to pass a binary in, you need to wrap it as {raw, Binary}, but % Usually to pass a binary in, you need to wrap it as {raw, Binary}, but
% since sg_... strings OR hex blobs can be used as signatures in Sophia, we % since sg_... strings OR hex blobs can be used as signatures in Sophia, we
% special case this case based on the length. Even if a binary starts with % special case this case based on the length. Even if a binary starts with
% "sg_", 64 characters is not enough to represent a 64 byte signature, so % "sg_", 64 characters is not enough to represent a 64 byte signature, so
% the most optimistic interpretation is to use the binary directly. % the most optimistic interpretation is to use the binary directly.
{ok, S}; {ok, S};
coerce({O, N, signature}, S, to_fate) -> erlang_to_fate({O, N, signature}, S) ->
coerce_chain_object(O, N, signature, signature, S); coerce_chain_object(O, N, signature, signature, S);
coerce({_, _, signature}, Bin, from_fate) -> %erlang_to_fate({_, _, channel}, S) when is_binary(S) ->
Address = gmser_api_encoder:encode(signature, Bin),
{ok, unicode:characters_to_list(Address)};
%coerce({_, _, channel}, S, to_fate) when is_binary(S) ->
%{ok, {channel, S}}; %{ok, {channel, S}};
%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) -> erlang_to_fate({_, _, boolean}, true) ->
%{ok, S};
coerce({_, _, boolean}, true, _) ->
{ok, true}; {ok, true};
coerce({_, _, boolean}, "true", _) -> erlang_to_fate({_, _, boolean}, "true") ->
{ok, true}; {ok, true};
coerce({_, _, boolean}, false, _) -> erlang_to_fate({_, _, boolean}, false) ->
{ok, false}; {ok, false};
coerce({_, _, boolean}, "false", _) -> erlang_to_fate({_, _, boolean}, "false") ->
{ok, false}; {ok, false};
coerce({O, N, boolean}, S, _) -> erlang_to_fate({O, N, string}, Str) ->
single_error({invalid, O, N, S}); case unicode:characters_to_binary(Str) of
coerce({O, N, string}, Str, Direction) ->
Result = case Direction of
to_fate -> unicode:characters_to_binary(Str);
from_fate -> unicode:characters_to_list(Str)
end,
case Result of
{error, _, _} -> {error, _, _} ->
single_error({invalid, O, N, Str}); single_error({invalid, O, N, Str});
{incomplete, _, _} -> {incomplete, _, _} ->
@ -539,9 +494,9 @@ coerce({O, N, string}, Str, Direction) ->
StrBin -> StrBin ->
{ok, StrBin} {ok, StrBin}
end; end;
coerce({_, _, char}, Val, _Direction) when is_integer(Val) -> erlang_to_fate({_, _, char}, Val) when is_integer(Val) ->
{ok, Val}; {ok, Val};
coerce({O, N, char}, Str, to_fate) -> erlang_to_fate({O, N, char}, Str) ->
Result = unicode:characters_to_list(Str), Result = unicode:characters_to_list(Str),
case Result of case Result of
{error, _, _} -> {error, _, _} ->
@ -553,27 +508,24 @@ coerce({O, N, char}, Str, to_fate) ->
_ -> _ ->
single_error({invalid, O, N, Str}) single_error({invalid, O, N, Str})
end; end;
coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) -> erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) ->
coerce_bytes(O, N, Count, Bytes); coerce_bytes(O, N, Count, Bytes);
coerce({_, _, bits}, {bits, Num}, from_fate) -> erlang_to_fate({_, _, bits}, Num) when is_integer(Num) ->
{ok, Num};
coerce({_, _, bits}, Num, to_fate) when is_integer(Num) ->
{ok, {bits, Num}}; {ok, {bits, Num}};
coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) -> erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) ->
Size = bit_size(Bits), Size = bit_size(Bits),
<<IntValue:Size>> = Bits, <<IntValue:Size>> = Bits,
{ok, {bits, IntValue}}; {ok, {bits, IntValue}};
coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) -> erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) ->
coerce_list(Type, Data, Direction); coerce_list(Type, Data, to_fate);
coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) -> erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, Direction); coerce_map(KeyType, ValType, Data, to_fate);
coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) -> erlang_to_fate({O, N, {tuple, ElementTypes}}, Data) when is_tuple(Data) ->
ElementList = tuple_to_list(Data), ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, to_fate); coerce_tuple(O, N, ElementTypes, ElementList, to_fate);
coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) -> erlang_to_fate({O, N, {variant, Variants}}, Name) when is_list(Name) ->
ElementList = tuple_to_list(Data), erlang_to_fate({O, N, {variant, Variants}}, {Name});
coerce_tuple(O, N, ElementTypes, ElementList, from_fate); erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_size(Data) > 0 ->
coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_size(Data) > 0 ->
[Name | Terms] = tuple_to_list(Data), [Name | Terms] = tuple_to_list(Data),
case lookup_variant(Name, Variants) of case lookup_variant(Name, Variants) of
{Tag, TermTypes} -> {Tag, TermTypes} ->
@ -582,17 +534,9 @@ coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_si
ValidNames = [Valid || {Valid, _} <- Variants], ValidNames = [Valid || {Valid, _} <- Variants],
single_error({invalid_variant, O, N, Name, ValidNames}) single_error({invalid_variant, O, N, Name, ValidNames})
end; end;
coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) -> erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) ->
coerce({O, N, {variant, Variants}}, {Name}, to_fate);
coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) ->
coerce_map_to_record(O, N, MemberTypes, Map); coerce_map_to_record(O, N, MemberTypes, Map);
coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) -> erlang_to_fate({O, N, {unknown_type, _}}, Data) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
coerce({O, N, {unknown_type, _}}, Data, _) ->
case N of case N of
already_normalized -> already_normalized ->
Message = "Warning: Unknown type ~p. Using term ~p as is.~n", Message = "Warning: Unknown type ~p. Using term ~p as is.~n",
@ -602,24 +546,7 @@ coerce({O, N, {unknown_type, _}}, Data, _) ->
io:format(Message, [O, N, Data]) io:format(Message, [O, N, Data])
end, end,
{ok, Data}; {ok, Data};
coerce({O, N, _}, Data, from_fate) -> erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}).
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
{ok, Data};
coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}).
coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
single_error({partial_bytes, O, N, bit_size(Bytes)});
coerce_bytes(_, _, any, Bytes) ->
{ok, Bytes};
coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
single_error({incorrect_size, O, N, Bytes});
coerce_bytes(_, _, _, Bytes) ->
{ok, Bytes}.
coerce_chain_object(_, _, _, _, {raw, Binary}) -> coerce_chain_object(_, _, _, _, {raw, Binary}) ->
{ok, Binary}; {ok, Binary};
@ -644,12 +571,38 @@ decode_chain_object(Tag, S) ->
error:incorrect_size -> {error, incorrect_size} error:incorrect_size -> {error, incorrect_size}
end. end.
coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
single_error({partial_bytes, O, N, bit_size(Bytes)});
coerce_bytes(_, _, any, Bytes) ->
{ok, Bytes};
coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
single_error({incorrect_size, O, N, Bytes});
coerce_bytes(_, _, _, Bytes) ->
{ok, Bytes}.
coerce_zipped_bindings(Bindings, Direction, Tag) ->
coerce_zipped_bindings(Bindings, Direction, Tag, [], []).
coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) ->
{{ArgName, Type}, Term} = Next,
case coerce_direction(Type, Term, Direction) of
{ok, NewTerm} ->
coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({Tag, ArgName}, Errors),
coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken])
end;
coerce_zipped_bindings([], _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_zipped_bindings([], _, _, _, Broken) ->
{error, combine_errors(Broken)}.
coerce_list(Type, Elements, Direction) -> coerce_list(Type, Elements, Direction) ->
% 0 index since it represents a sophia list % 0 index since it represents a sophia list
coerce_list(Type, Elements, Direction, 0, [], []). coerce_list(Type, Elements, Direction, 0, [], []).
coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) -> coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) ->
case coerce(Type, Next, Direction) of case coerce_direction(Type, Next, Direction) of
{ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken); {ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken);
{error, Errors} -> {error, Errors} ->
Wrapped = wrap_errors({index, Index}, Errors), Wrapped = wrap_errors({index, Index}, Errors),
@ -672,7 +625,7 @@ coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) ->
end. end.
coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) -> coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
case coerce(KeyType, K, Direction) of case coerce_direction(KeyType, K, Direction) of
{ok, KFATE} -> {ok, KFATE} ->
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE); coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE);
{error, Errors} -> {error, Errors} ->
@ -683,7 +636,7 @@ coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
end. end.
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) -> coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) ->
case coerce(ValType, V, Direction) of case coerce_direction(ValType, V, Direction) of
{ok, VFATE} -> {ok, VFATE} ->
NewGood = Good#{KFATE => VFATE}, NewGood = Good#{KFATE => VFATE},
coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken); coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken);
@ -720,11 +673,6 @@ coerce_tuple(O, N, TermTypes, Terms, Direction) ->
Errors -> Errors Errors -> Errors
end. end.
% Wraps a single error in a list, along with an empty path, so that other
% accumulating error handlers can work with it.
single_error(Reason) ->
{error, [{Reason, []}]}.
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) ->
% FIXME: we could go through and add the variant tag to the adt_element % FIXME: we could go through and add the variant tag to the adt_element
% paths? % paths?
@ -751,7 +699,7 @@ coerce_tuple_elements(Types, Terms, Direction, Tag) ->
coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []). coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []).
coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) -> coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) ->
case coerce(Type, Term, Direction) of case coerce_direction(Type, Term, Direction) of
{ok, Value} -> {ok, Value} ->
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken); coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken);
{error, Errors} -> {error, Errors} ->
@ -820,6 +768,91 @@ zip_record_field({Name, Type}, {Remaining, Missing}) ->
{missing, {Remaining, [Name | Missing]}} {missing, {Remaining, [Name | Missing]}}
end. end.
% Wraps a single error in a list, along with an empty path, so that other
% accumulating error handlers can work with it.
single_error(Reason) ->
{error, [{Reason, []}]}.
wrap_errors(Location, Errors) ->
F = fun({Error, Path}) ->
{Error, [Location | Path]}
end,
lists:map(F, Errors).
combine_errors(Broken) ->
F = fun(NextErrors, Acc) ->
NextErrors ++ Acc
end,
lists:foldl(F, [], Broken).
%%% FATE to Erlang
% Not sure if this is needed... fate_to_erlang shouldn't fail.
coerce_direction(Type, Term, to_fate) ->
erlang_to_fate(Type, Term);
coerce_direction(Type, Term, from_fate) ->
fate_to_erlang(Type, Term).
fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
{ok, S};
fate_to_erlang({_, _, address}, {address, Bin}) ->
Address = gmser_api_encoder:encode(account_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
fate_to_erlang({_, _, contract}, {contract, Bin}) ->
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
fate_to_erlang({_, _, signature}, Bin) ->
Address = gmser_api_encoder:encode(signature, Bin),
{ok, unicode:characters_to_list(Address)};
%fate_to_erlang({_, _, channel}, {channel, S}) when is_binary(S) ->
%{ok, S};
fate_to_erlang({_, _, boolean}, true) ->
{ok, true};
fate_to_erlang({_, _, boolean}, false) ->
{ok, false};
fate_to_erlang({_, _, string}, Bin) ->
Str = binary_to_list(Bin),
{ok, Str};
fate_to_erlang({_, _, char}, Val) ->
{ok, Val};
fate_to_erlang({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) ->
coerce_bytes(O, N, Count, Bytes);
fate_to_erlang({_, _, bits}, {bits, Num}) ->
{ok, Num};
fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) ->
coerce_list(Type, Data, from_fate);
fate_to_erlang({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, from_fate);
fate_to_erlang({O, N, {tuple, ElementTypes}}, {tuple, Data}) ->
ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, from_fate);
fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
case N of
already_normalized ->
Message = "Warning: Unknown type ~p. Using term ~p as is.~n",
io:format(Message, [O, Data]);
_ ->
Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n",
io:format(Message, [O, N, Data])
end,
{ok, Data};
fate_to_erlang({O, N, _}, Data) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
{ok, Data}.
%%% AACI Getters %%% AACI Getters
@ -841,38 +874,44 @@ aaci_get_function_signature({aaci, _, FunDefs, _}, Fun) ->
end. end.
%%% Simple coerce/3 tests %%% Simple FATE/erlang tests
% Round trip coerce run for the eunit tests below. If these results don't match check_erlang_to_fate(Type, Sophia, Fate) ->
% then the test should fail. {ok, FateActual} = erlang_to_fate(Type, Sophia),
try_coerce(Type, Sophia, Fate) ->
% Run both first, to see if they fail to produce any result.
{ok, FateActual} = coerce(Type, Sophia, to_fate),
{ok, SophiaActual} = coerce(Type, Fate, from_fate),
% Now check that the results were what we expected.
case FateActual of case FateActual of
Fate -> Fate ->
ok; ok;
_ -> _ ->
erlang:error({to_fate_failed, Fate, FateActual}) erlang:error({to_fate_failed, Fate, FateActual})
end, end.
check_fate_to_erlang(Type, Fate, Sophia) ->
{ok, SophiaActual} = fate_to_erlang(Type, Fate),
% Now check that the results were what we expected.
case SophiaActual of case SophiaActual of
Sophia -> Sophia ->
ok; ok;
_ -> _ ->
erlang:error({from_fate_failed, Sophia, SophiaActual}) erlang:error({from_fate_failed, Sophia, SophiaActual})
end, end.
% Round trip coerce run for the eunit tests below. If these results don't match
% then the test should fail.
check_roundtrip(Type, Sophia, Fate) ->
check_erlang_to_fate(Type, Sophia, Fate),
check_fate_to_erlang(Type, Fate, Sophia),
% Finally, check that the FATE result is something that gmb understands. % Finally, check that the FATE result is something that gmb understands.
gmb_fate_encoding:serialize(Fate), gmb_fate_encoding:serialize(Fate),
ok. ok.
coerce_int_test() -> coerce_int_test() ->
{ok, Type} = annotate_type(integer, #{}), {ok, Type} = annotate_type(integer, #{}),
try_coerce(Type, 123, 123). check_roundtrip(Type, 123, 123).
coerce_address_test() -> coerce_address_test() ->
{ok, Type} = annotate_type(address, #{}), {ok, Type} = annotate_type(address, #{}),
try_coerce(Type, check_roundtrip(Type,
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx", "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123, {address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9, 167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
@ -880,7 +919,7 @@ coerce_address_test() ->
coerce_contract_test() -> coerce_contract_test() ->
{ok, Type} = annotate_type(contract, #{}), {ok, Type} = annotate_type(contract, #{}),
try_coerce(Type, check_roundtrip(Type,
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx", "ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123, {contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9, 167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
@ -888,7 +927,7 @@ coerce_contract_test() ->
coerce_signature_test() -> coerce_signature_test() ->
{ok, Type} = annotate_type(signature, #{}), {ok, Type} = annotate_type(signature, #{}),
try_coerce(Type, check_roundtrip(Type,
"sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n", "sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n",
<<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135, <<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
@ -901,69 +940,69 @@ coerce_signature_binary_test() ->
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114, 249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
233,110,119,143,206,137,124,36,63,154,85,7>>, 233,110,119,143,206,137,124,36,63,154,85,7>>,
{ok, Binary} = coerce(Type, {raw, Binary}, to_fate), {ok, Binary} = erlang_to_fate(Type, {raw, Binary}),
{ok, Binary} = coerce(Type, Binary, to_fate), {ok, Binary} = erlang_to_fate(Type, Binary),
ok. ok.
coerce_bool_test() -> coerce_bool_test() ->
{ok, Type} = annotate_type(boolean, #{}), {ok, Type} = annotate_type(boolean, #{}),
try_coerce(Type, true, true), check_roundtrip(Type, true, true),
try_coerce(Type, false, false). check_roundtrip(Type, false, false).
coerce_string_test() -> coerce_string_test() ->
{ok, Type} = annotate_type(string, #{}), {ok, Type} = annotate_type(string, #{}),
try_coerce(Type, "hello world", <<"hello world">>). check_roundtrip(Type, "hello world", <<"hello world">>).
coerce_list_test() -> coerce_list_test() ->
{ok, Type} = annotate_type({list, [string]}, #{}), {ok, Type} = annotate_type({list, [string]}, #{}),
try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). check_roundtrip(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
coerce_map_test() -> coerce_map_test() ->
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}), {ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}),
try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). check_roundtrip(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
coerce_tuple_test() -> coerce_tuple_test() ->
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}), {ok, Type} = annotate_type({tuple, [integer, string]}, #{}),
try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}). check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
coerce_variant_test() -> coerce_variant_test() ->
{ok, Type} = annotate_type({variant, [{"A", [integer]}, {ok, Type} = annotate_type({variant, [{"A", [integer]},
{"B", [integer, integer]}]}, {"B", [integer, integer]}]},
#{}), #{}),
try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), check_roundtrip(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). check_roundtrip(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
coerce_option_test() -> coerce_option_test() ->
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()), {ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}), check_roundtrip(Type, {"None"}, {variant, [0, 1], 0, {}}),
try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}). check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
coerce_record_test() -> coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
coerce_bytes_test() -> coerce_bytes_test() ->
{ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}),
try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}).
coerce_bits_test() -> coerce_bits_test() ->
{ok, Type} = annotate_type(bits, #{}), {ok, Type} = annotate_type(bits, #{}),
try_coerce(Type, 5, {bits, 5}). check_roundtrip(Type, 5, {bits, 5}).
coerce_char_test() -> coerce_char_test() ->
{ok, Type} = annotate_type(char, #{}), {ok, Type} = annotate_type(char, #{}),
try_coerce(Type, $?, $?). check_roundtrip(Type, $?, $?).
coerce_unicode_test() -> coerce_unicode_test() ->
{ok, Type} = annotate_type(char, #{}), {ok, Type} = annotate_type(char, #{}),
% Latin Small Letter C with cedilla and acute % Latin Small Letter C with cedilla and acute
{ok, $ḉ} = coerce(Type, <<""/utf8>>, to_fate), {ok, $ḉ} = erlang_to_fate(Type, <<""/utf8>>),
ok. ok.
coerce_hash_test() -> coerce_hash_test() ->
{ok, Type} = annotate_type("hash", builtin_typedefs()), {ok, Type} = annotate_type("hash", builtin_typedefs()),
Hash = list_to_binary(lists:seq(1,32)), Hash = list_to_binary(lists:seq(1,32)),
try_coerce(Type, Hash, Hash), check_roundtrip(Type, Hash, Hash),
ok. ok.
@ -985,7 +1024,7 @@ namespace_coerce_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
record_substitution_test() -> record_substitution_test() ->
Contract = " Contract = "
@ -995,7 +1034,7 @@ record_substitution_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
tuple_substitution_test() -> tuple_substitution_test() ->
Contract = " Contract = "
@ -1005,7 +1044,7 @@ tuple_substitution_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}). check_roundtrip(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
variant_substitution_test() -> variant_substitution_test() ->
Contract = " Contract = "
@ -1015,8 +1054,8 @@ variant_substitution_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}), check_roundtrip(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
try_coerce(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}). check_roundtrip(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
nested_coerce_test() -> nested_coerce_test() ->
Contract = " Contract = "
@ -1027,7 +1066,7 @@ nested_coerce_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, check_roundtrip(Output,
#{ "f1" => {1, 2}, "f2" => {"a", "b"}}, #{ "f1" => {1, 2}, "f2" => {"a", "b"}},
{tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}). {tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}).
@ -1039,7 +1078,7 @@ state_coerce_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "init"), {ok, {[], Output}} = aaci_get_function_signature(AACI, "init"),
try_coerce(Output, 0, 0). check_roundtrip(Output, 0, 0).
param_test() -> param_test() ->
Contract = " Contract = "
@ -1049,8 +1088,8 @@ param_test() ->
", ",
{ok, AACI} = aaci_from_string(Contract), {ok, AACI} = aaci_from_string(Contract),
{ok, {[{"x", Input}], Output}} = aaci_get_function_signature(AACI, "init"), {ok, {[{"x", Input}], Output}} = aaci_get_function_signature(AACI, "init"),
try_coerce(Input, 0, 0), check_roundtrip(Input, 0, 0),
try_coerce(Output, 0, 0). check_roundtrip(Output, 0, 0).
%%% Obscure Sophia types where we should check the AACI as well %%% Obscure Sophia types where we should check the AACI as well
@ -1126,21 +1165,21 @@ name_coerce_test() ->
{ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()), {ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()),
TTLSoph = {"FixedTTL", 0}, TTLSoph = {"FixedTTL", 0},
TTLFate = {variant, [1, 1], 0, {0}}, TTLFate = {variant, [1, 1], 0, {0}},
try_coerce(TTL, TTLSoph, TTLFate), check_roundtrip(TTL, TTLSoph, TTLFate),
{ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()), {ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()),
PointeeSoph = {"AccountPt", AddrSoph}, PointeeSoph = {"AccountPt", AddrSoph},
PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}}, PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}},
try_coerce(Pointee, PointeeSoph, PointeeFate), check_roundtrip(Pointee, PointeeSoph, PointeeFate),
{ok, Name} = annotate_type("AENS.name", builtin_typedefs()), {ok, Name} = annotate_type("AENS.name", builtin_typedefs()),
NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}}, NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}},
NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}}, NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}},
try_coerce(Name, NameSoph, NameFate). check_roundtrip(Name, NameSoph, NameFate).
void_coerce_test() -> void_coerce_test() ->
% Void itself can't be represented, but other types built out of void are % Void itself can't be represented, but other types built out of void are
% valid. % valid.
{ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()), {ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()),
try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}), check_roundtrip(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
{ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()), {ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()),
try_coerce(NonList, [], []). check_roundtrip(NonList, [], []).