Files
hakuzaru/src/hz_aaci.erl
T
Jarvis Carroll ed252b4c06 Also note index in record_element
I changed it from noting the index to just noting the field name, but
actually both pieces of information are important, since if there was
a type error, presumably the type information is actually wrong.

Now we put the index first, since that is the part of the FATE tuple
that failed, and then the field name that that would be if the type
information were correct, in case that is useful.
2026-05-12 06:07:58 +00:00

1321 lines
55 KiB
Erlang

%%% @doc
%%% Sophia datatype manipulations for Hakuzaru
%%%
%%% Sophia and FATE are two subtly different machine languages with two
%%% different type systems. Application developers probably think about their
%%% smart contracts in terms of the Sophia types, but the node will only accept
%%% the corresponding FATE types, and both of these type systems result in
%%% different erlang terms for representing the same thing. This module defines
%%% the conversion between these different representations of the same data.
%%% @end
-module(hz_aaci).
-vsn("0.9.1").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
% Contract call and serialization interface functions
-export([prepare_from_file/1,
prepare/1,
erlang_to_fate/2,
fate_to_erlang/2,
erlang_args_to_fate/2,
get_function_signature/2]).
%%% Types
-export_type([aaci/0, annotated_type/0, erlang_repr/0]).
-include_lib("eunit/include/eunit.hrl").
-type aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
-type typedef() :: {[string()], typedef_rhs()}.
-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), builtin_type(annotated_type())}.
-type builtin_type(T) :: {bytes, [integer() | any]}
| {record, [{string(), T}]}
| {variant, [{string(), [T]}]}
| {tuple, [T]}
| {list, [T]}
| {map, [T]}
| integer
| boolean
| bits
| char
| string
| address
| signature
| contract
| channel
| unknown_type.
-type opaque_type() :: string() | {string(), [opaque_type()]} | builtin_type(opaque_type()).
-type typedef_rhs() :: {var, string()} | string() | {string(), [opaque_type()]} | builtin_type(typedef_rhs()).
-type erlang_repr() :: integer()
| string()
| boolean()
| binary()
| tuple() % Tuples, variants, or raw addresses
| [erlang_repr()]
| #{erlang_repr() => erlang_repr()}.
%%% ACI/AACI
-spec prepare_from_file(Path) -> {ok, AACI} | {error, Reason}
when Path :: file:filename(),
AACI :: aaci(),
Reason :: term().
%% @doc
%% Compile a contract and extract the function spec meta for use in future formation
%% of calldata
prepare_from_file(Path) ->
case so_compiler:file(Path, [{aci, json}]) of
{ok, #{aci := ACI}} -> {ok, prepare(ACI)};
Error -> Error
end.
-spec prepare(ACI) -> AACI
when ACI :: term(),
AACI :: aaci().
prepare(ACI) ->
% We want to take the types represented by the ACI, things like N1.T(N2.T),
% and dereference them down to concrete types like
% {tuple, [integer, string]}. Our type dereferencing algorithms
% shouldn't act directly on the JSON-based structures that the compiler
% gives us, though, though, so before we do the analysis, we should strip
% the ACI down to a list of 'opaque' type defintions and function specs.
{Name, OpaqueSpecs, TypeDefs} = convert_aci_types(ACI),
% Now that we have the opaque types, we can dereference the function specs
% down to the concrete types they actually represent. We annotate each
% subexpression of this concrete type with other info too, in case it helps
% make error messages easier to understand.
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
{aaci, Name, Specs, TypeDefs}.
-spec convert_aci_types(ACI) -> {Name, OpaqueSpecs, TypeDefs}
when ACI :: term(),
Name :: string(),
OpaqueSpecs :: [{string(), [{string(), opaque_type()}], opaque_type()}],
TypeDefs :: #{string() => typedef()}.
convert_aci_types(ACI) ->
% Find the main contract, so we can get the specifications of its
% entrypoints.
[{NameBin, SpecDefs}] =
[{N, F}
|| #{contract := #{kind := contract_main,
functions := F,
name := N}} <- ACI],
Name = binary_to_list(NameBin),
% Turn these specifications into opaque types that we can reason about.
Specs = lists:map(fun convert_function_spec/1, SpecDefs),
% These specifications can reference other type definitions from the main
% contract and any other namespaces, so extract these types and convert
% them too.
TypeDefTree = lists:map(fun convert_namespace_typedefs/1, ACI),
% The tree structure of the ACI naturally leads to a tree of opaque types,
% but we want a map, so flatten it out before we continue.
TypeDefMap = collect_opaque_types(TypeDefTree, #{}),
% This is all the information we actually need from the ACI, the rest is
% just pre-compute and acceleration.
{Name, Specs, TypeDefMap}.
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
Name = binary_to_list(NameBin),
ArgTypes = lists:map(fun convert_arg/1, Args),
ResultType = opaque_type([], Result),
{Name, ArgTypes, ResultType}.
convert_arg(#{name := NameBin, type := TypeDef}) ->
Name = binary_to_list(NameBin),
Type = opaque_type([], TypeDef),
{Name, Type}.
convert_namespace_typedefs(#{namespace := NS}) ->
Name = namespace_name(NS),
convert_typedefs(NS, Name);
convert_namespace_typedefs(#{contract := NS}) ->
Name = namespace_name(NS),
ImplicitTypes = convert_implicit_types(NS, Name),
ExplicitTypes = convert_typedefs(NS, Name),
[ImplicitTypes, ExplicitTypes].
namespace_name(#{name := NameBin}) ->
binary_to_list(NameBin).
convert_implicit_types(#{state := StateDefACI}, Name) ->
StateDefOpaque = opaque_type([], StateDefACI),
[{Name, [], contract},
{Name ++ ".state", [], StateDefOpaque}];
convert_implicit_types(_, Name) ->
[{Name, [], contract}].
convert_typedefs(#{typedefs := TypeDefs}, Name) ->
convert_typedefs_loop(TypeDefs, Name ++ ".", []).
% Take a namespace that has already had a period appended, and use that as a
% prefix to convert and annotate a list of types.
convert_typedefs_loop([], _NamePrefix, Converted) ->
Converted;
convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
#{name := NameBin, vars := ParamDefs, typedef := DefACI} = Next,
Name = NamePrefix ++ binary_to_list(NameBin),
Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs],
Def = opaque_type(Params, DefACI),
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]).
-spec collect_opaque_types(Tree, TypeDefs) -> TypeDefs
when Tree :: typedef_tree(),
TypeDefs :: #{string() => typedef()}.
-type typedef_tree() :: {string(), [string()], typedef_rhs()} | list(typedef_tree()).
collect_opaque_types([], Types) ->
Types;
collect_opaque_types([L | R], Types) ->
NewTypes = collect_opaque_types(L, Types),
collect_opaque_types(R, NewTypes);
collect_opaque_types({Name, Params, Def}, Types) ->
maps:put(Name, {Params, Def}, Types).
%%% ACI Type -> Opaque Type
-spec opaque_type(Params, ACIType) -> Opaque
when Params :: [string()],
ACIType :: binary() | map(),
Opaque :: typedef_rhs().
% Convert an ACI type defintion/spec into the 'opaque type' representation that
% our dereferencing algorithms can reason about.
opaque_type(Params, NameBin) when is_binary(NameBin) ->
Name = opaque_type_name(NameBin),
case not is_atom(Name) and lists:member(Name, Params) of
false -> Name;
true -> {var, Name}
end;
opaque_type(Params, #{record := FieldDefs}) ->
Fields = [{binary_to_list(Name), opaque_type(Params, Type)}
|| #{name := Name, type := Type} <- FieldDefs],
{record, Fields};
opaque_type(Params, #{variant := VariantDefs}) ->
ConvertVariant = fun(Pair) ->
[{Name, Types}] = maps:to_list(Pair),
{binary_to_list(Name), [opaque_type(Params, Type) || Type <- Types]}
end,
Variants = lists:map(ConvertVariant, VariantDefs),
{variant, Variants};
opaque_type(Params, #{tuple := TypeDefs}) ->
{tuple, [opaque_type(Params, Type) || Type <- TypeDefs]};
opaque_type(_, #{bytes := Count}) ->
{bytes, [Count]};
opaque_type(Params, Pair) when is_map(Pair) ->
[{Name, TypeArgs}] = maps:to_list(Pair),
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}.
-spec opaque_type_name(binary()) -> atom() | string().
% Atoms for any builtins that aren't qualified by a namespace in Sophia.
% Everything else stays as a string, user-defined or not.
opaque_type_name(<<"int">>) -> integer;
opaque_type_name(<<"bool">>) -> boolean;
opaque_type_name(<<"bits">>) -> bits;
opaque_type_name(<<"char">>) -> char;
opaque_type_name(<<"string">>) -> string;
opaque_type_name(<<"address">>) -> address;
opaque_type_name(<<"signature">>) -> signature;
opaque_type_name(<<"contract">>) -> contract;
opaque_type_name(<<"list">>) -> list;
opaque_type_name(<<"map">>) -> map;
% I'm not sure how to produce channels in Sophia source, but they seem to exist
% in gmb still.
opaque_type_name(<<"channel">>) -> channel;
opaque_type_name(Name) -> binary_to_list(Name).
builtin_typedefs() ->
#{"unit" => {[], {tuple, []}},
"void" => {[], {variant, []}},
"hash" => {[], {bytes, [32]}},
"option" => {["'T"], {variant, [{"None", []},
{"Some", [{var, "'T"}]}]}},
"Chain.ttl" => {[], {variant, [{"FixedTTL", [integer]},
{"RelativeTTL", [integer]}]}},
"AENS.pointee" => {[], {variant, [{"AccountPt", [address]},
{"OraclePt", [address]},
{"ContractPt", [address]},
{"ChannelPt", [address]}]}},
"AENS.name" => {[], {variant, [{"Name", [address,
"Chain.ttl",
{map, [string, "AENS.pointee"]}]}]}},
"AENSv2.pointee" => {[], {variant, [{"AccountPt", [address]},
{"OraclePt", [address]},
{"ContractPt", [address]},
{"ChannelPt", [address]},
{"DataPt", [{bytes, [any]}]}]}},
"AENSv2.name" => {[], {variant, [{"Name", [address,
"Chain.ttl",
{map, [string, "AENSv2.pointee"]}]}]}},
"Chain.ga_meta_tx" => {[], {variant, [{"GAMetaTx", [address, integer]}]}},
"Chain.paying_for_tx" => {[], {variant, [{"PayingForTx", [address, integer]}]}},
"Chain.base_tx" => {[], {variant, [{"SpendTx", [address, integer, string]},
{"OracleRegisterTx", []},
{"OracleQueryTx", []},
{"OracleResponseTx", []},
{"OracleExtendTx", []},
{"NamePreclaimTx", []},
{"NameClaimTx", ["hash"]},
{"NameUpdateTx", [string]},
{"NameRevokeTx", ["hash"]},
{"NameTransferTx", [address, string]},
{"ChannelCreateTx", [address]},
{"ChannelDepositTx", [address, integer]},
{"ChannelWithdrawTx", [address, integer]},
{"ChannelForceProgressTx", [address]},
{"ChannelCloseMutualTx", [address]},
{"ChannelCloseSoloTx", [address]},
{"ChannelSlashTx", [address]},
{"ChannelSettleTx", [address]},
{"ChannelSnapshotSoloTx", [address]},
{"ContractCreateTx", [integer]},
{"ContractCallTx", [address, integer]},
{"GAAttachTx", []}]}},
"Chain.tx" => {[], {record, [{"paying_for", {"option", ["Chain.paying_for_tx"]}},
{"ga_metas", {list, ["Chain.ga_meta_tx"]}},
{"actor", address},
{"fee", integer},
{"ttl", integer},
{"tx", "Chain.base_tx"}]}},
"MCL_BLS12_381.fr" => {[], {bytes, [32]}},
"MCL_BLS12_381.fp" => {[], {bytes, [48]}}
}.
%%% Opaque Type -> Accelerated 'Annotated' Type
% Type preparation has two goals. First, we need a data structure that can be
% traversed quickly, to take sophia-esque erlang expressions and turn them into
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need
% partially substituted names, so that error messages can be generated for why
% "foobar" is not valid as the third field of a `bazquux`, because the third
% field is supposed to be `option(integer)`, not `string`.
%
% To achieve this we need three representations of each type expression, which
% together form an 'annotated type'. First, we need the fully opaque name,
% "bazquux", then we need the normalized name, which is an opaque name with the
% bare-minimum substitution needed to make the outer-most type-constructor an
% identifiable built-in, ADT, or record type, and then we need the dereferenced
% type, which is the raw {variant, [{Name, Fields}, ...]} or
% {record, [{Name, Type}]} expression that can be used in actual Sophia->FATE
% coercion. The type sub-expressions in these dereferenced types will each be
% fully annotated as well, i.e. they will each contain *all three* of the above
% representations, so that coercion of subexpressions remains fast AND
% informative.
%
% In a lot of cases the opaque type given will already be normalized, in which
% case either the normalized field or the non-normalized field of an annotated
% type can simple be the atom `already_normalized`, which means error messages
% can simply render the normalized type expression and know that the error will
% make sense.
annotate_function_specs([], _Types, Specs) ->
Specs;
annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs) ->
{ok, Args} = annotate_bindings(ArgsOpaque, Types, []),
{ok, Result} = annotate_type(ResultOpaque, Types),
NewSpecs = maps:put(Name, {Args, Result}, Specs),
annotate_function_specs(Rest, Types, NewSpecs).
-spec annotate_type(Opaque, Types) -> {ok, Annotated} | {error, Reason}
when Opaque :: opaque_type(),
Types :: #{string() => typedef()},
Annotated :: annotated_type(),
Reason :: none().
annotate_type(T, Types) ->
case normalize_opaque_type(T, Types) of
{ok, AlreadyNormalized, NOpaque, NExpanded} ->
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types);
Error ->
Error
end.
annotate_type2(T, _, _, unknown_type, _) ->
% If a type is unknown, then it should not be reported as the normalized
% name.
{ok, {T, unknown_type, unknown_type}};
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
case annotate_type_subexpressions(NExpanded, Types) of
{ok, Flat} ->
case AlreadyNormalized of
true -> {ok, {T, already_normalized, Flat}};
false -> {ok, {T, NOpaque, Flat}}
end;
Error ->
Error
end.
annotate_types([T | Rest], Types, Acc) ->
case annotate_type(T, Types) of
{ok, Type} -> annotate_types(Rest, Types, [Type | Acc]);
Error -> Error
end;
annotate_types([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) ->
{ok, PrimitiveType};
annotate_type_subexpressions({bytes, [Count]}, _Types) ->
% bytes is weird, because it has an argument, but that argument isn't an
% opaque type.
{ok, {bytes, [Count]}};
annotate_type_subexpressions({variant, VariantsOpaque}, Types) ->
case annotate_variants(VariantsOpaque, Types, []) of
{ok, Variants} -> {ok, {variant, Variants}};
Error -> Error
end;
annotate_type_subexpressions({record, FieldsOpaque}, Types) ->
case annotate_bindings(FieldsOpaque, Types, []) of
{ok, Fields} -> {ok, {record, Fields}};
Error -> Error
end;
annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
case annotate_types(ElemsOpaque, Types, []) of
{ok, Elems} -> {ok, {T, Elems}};
Error -> Error
end.
annotate_bindings([{Name, T} | Rest], Types, Acc) ->
case annotate_type(T, Types) of
{ok, Type} -> annotate_bindings(Rest, Types, [{Name, Type} | Acc]);
Error -> Error
end;
annotate_bindings([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
case annotate_types(Elems, Types, []) of
{ok, ElemsFlat} -> annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
Error -> Error
end;
annotate_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
% This function evaluates type aliases in a loop, until eventually a usable
% definition is found.
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true).
% FIXME detect infinite loops
% FIXME detect builtins with the wrong number of arguments
% FIXME should nullary types have an empty list of arguments added before now?
normalize_opaque_type(T, _Types, IsFirst) when is_atom(T) ->
% Once we have eliminated the above rewrite cases, all other cases are
% handled explicitly by the coerce logic, and so are considered normalized.
{ok, IsFirst, T, T};
normalize_opaque_type(Type = {T, _}, _Types, IsFirst) when is_atom(T) ->
% Once we have eliminated the above rewrite cases, all other cases are
% handled explicitly by the coerce logic, and so are considered normalized.
{ok, IsFirst, Type, Type};
normalize_opaque_type(T, Types, IsFirst) when is_list(T) ->
% Lists/strings indicate userspace types, which may require arg
% substitutions. Convert to an explicit but empty arg list, for uniformity.
normalize_opaque_type({T, []}, Types, IsFirst);
normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) ->
case maps:find(T, Types) of
error ->
% We couldn't find this named type... Keep building the AACI, but
% mark this type expression as unknown, so that FATE coercions
% aren't attempted.
{ok, IsFirst, {T, TypeArgs}, unknown_type};
{ok, {TypeParamNames, Definition}} ->
% We have a definition for this type, including names for whatever
% args we have been given. Subtitute our args into this.
NewType = substitute_opaque_type(TypeParamNames, Definition, TypeArgs),
% Now continue on to see if we need to restart the loop or not.
normalize_opaque_type2(IsFirst, {T, TypeArgs}, NewType, Types)
end.
normalize_opaque_type2(IsFirst, PrevType, NextType = {variant, _}, _) ->
% We have reduced to a variant. Report the type name as the normalized
% type, but also provide the variant definition itself as the candidate
% flattened type for further annotation.
{ok, IsFirst, PrevType, NextType};
normalize_opaque_type2(IsFirst, PrevType, NextType = {record, _}, _) ->
% We have reduced to a record. Report the type name as the normalized
% type, but also provide the record definition itself as the candidate
% flattened type for further annotation.
{ok, IsFirst, PrevType, NextType};
normalize_opaque_type2(_, _, NextType, Types) ->
% Not a variant or record yet, so go back to the start of the loop.
% It will no longer be the first iteration.
normalize_opaque_type(NextType, Types, false).
% Perform a beta-reduction on a type expression.
substitute_opaque_type([], Definition, _) ->
% There are no parameters to substitute. This is the simplest way of
% defining type aliases, records, and variants, so we should make sure to
% short circuit all the recursive descent logic, since it won't actually
% do anything.
Definition;
substitute_opaque_type(TypeParamNames, Definition, TypeArgs) ->
% Bundle the param names alongside the args that we want to substitute, so
% that we can keyfind the one list.
Bindings = lists:zip(TypeParamNames, TypeArgs),
substitute_opaque_type(Bindings, Definition).
substitute_opaque_type(Bindings, {var, VarName}) ->
case lists:keyfind(VarName, 1, Bindings) of
{_, TypeArg} -> TypeArg;
% No valid ACI will create this case. Regardless, the user should
% still be able to specify arbitrary gmb FATE terms for whatever this
% is meant to be.
false -> unknown_type
end;
substitute_opaque_type(Bindings, {variant, Variants}) ->
Each = fun({VariantName, Elements}) ->
NewElements = substitute_opaque_types(Bindings, Elements),
{VariantName, NewElements}
end,
NewVariants = lists:map(Each, Variants),
{variant, NewVariants};
substitute_opaque_type(Bindings, {record, Fields}) ->
Each = fun({FieldName, FieldType}) ->
NewType = substitute_opaque_type(Bindings, FieldType),
{FieldName, NewType}
end,
NewFields = lists:map(Each, Fields),
{record, NewFields};
substitute_opaque_type(Bindings, {Connective, Args}) ->
NewArgs = substitute_opaque_types(Bindings, Args),
{Connective, NewArgs};
substitute_opaque_type(_Bindings, Type) ->
Type.
substitute_opaque_types(Bindings, Types) ->
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types).
%%% Erlang to FATE
-spec erlang_args_to_fate(VarTypes, Terms) -> {ok, FATE} | {error, Errors}
when VarTypes :: [{string(), annotated_type()}],
Terms :: [erlang_repr()],
FATE :: gmb_fate_data:fate_type(),
Errors :: [{Reason, [PathStep]}],
Reason :: term(),
PathStep :: term().
erlang_args_to_fate(VarTypes, Terms) ->
DefLength = length(VarTypes),
ArgLength = length(Terms),
if
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg);
DefLength > ArgLength -> {error, too_few_args};
DefLength < ArgLength -> {error, too_many_args}
end.
-spec erlang_to_fate(Type, Erlang) -> {ok, FATE} | {error, Errors}
when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(),
Erlang :: erlang_repr(),
Errors :: [{Reason, [PathStep]}],
Reason :: term(),
PathStep :: term().
erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
{ok, S};
erlang_to_fate({O, N, integer}, S) when is_list(S) ->
try
Val = list_to_integer(S),
{ok, Val}
catch
error:badarg -> single_error({invalid, O, N, S})
end;
erlang_to_fate({O, N, address}, S) ->
coerce_chain_object(O, N, address, account_pubkey, S);
erlang_to_fate({O, N, contract}, S) ->
coerce_chain_object(O, N, contract, contract_pubkey, S);
erlang_to_fate({_, _, signature}, S) when is_binary(S) andalso (byte_size(S) =:= 64) ->
% 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
% 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
% the most optimistic interpretation is to use the binary directly.
{ok, S};
erlang_to_fate({O, N, signature}, S) ->
coerce_chain_object(O, N, signature, signature, S);
%erlang_to_fate({_, _, channel}, S) when is_binary(S) ->
%{ok, {channel, S}};
erlang_to_fate({_, _, boolean}, true) ->
{ok, true};
erlang_to_fate({_, _, boolean}, "true") ->
{ok, true};
erlang_to_fate({_, _, boolean}, false) ->
{ok, false};
erlang_to_fate({_, _, boolean}, "false") ->
{ok, false};
erlang_to_fate({O, N, string}, Str) ->
case unicode:characters_to_binary(Str) of
{error, _, _} ->
single_error({invalid, O, N, Str});
{incomplete, _, _} ->
single_error({invalid, O, N, Str});
StrBin ->
{ok, StrBin}
end;
erlang_to_fate({_, _, char}, Val) when is_integer(Val) ->
{ok, Val};
erlang_to_fate({O, N, char}, Str) ->
Result = unicode:characters_to_list(Str),
case Result of
{error, _, _} ->
single_error({invalid, O, N, Str});
{incomplete, _, _} ->
single_error({invalid, O, N, Str});
[C] ->
{ok, C};
_ ->
single_error({invalid, O, N, Str})
end;
erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) ->
coerce_bytes(O, N, Count, Bytes);
erlang_to_fate({_, _, bits}, Num) when is_integer(Num) ->
{ok, {bits, Num}};
erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) ->
Size = bit_size(Bits),
<<IntValue:Size>> = Bits,
{ok, {bits, IntValue}};
erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) ->
coerce_list(Type, Data, to_fate);
erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, to_fate);
erlang_to_fate({O, N, {tuple, ElementTypes}}, Data) when is_tuple(Data) ->
ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, to_fate);
erlang_to_fate({O, N, {variant, Variants}}, Name) when is_list(Name) ->
erlang_to_fate({O, N, {variant, Variants}}, {Name});
erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_size(Data) > 0 ->
[Name | Terms] = tuple_to_list(Data),
case lookup_variant(Name, Variants) of
{Tag, TermTypes} ->
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, to_fate);
not_found ->
ValidNames = [Valid || {Valid, _} <- Variants],
single_error({invalid_variant, O, N, Name, ValidNames})
end;
erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) ->
coerce_map_to_record(O, N, MemberTypes, Map);
erlang_to_fate({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};
erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}).
coerce_chain_object(_, _, _, _, {raw, Binary}) ->
{ok, Binary};
coerce_chain_object(O, N, T, Tag, S) ->
case decode_chain_object(Tag, S) of
{ok, Data} -> {ok, coerce_chain_object2(T, Data)};
{error, Reason} -> single_error({Reason, O, N, S})
end.
coerce_chain_object2(address, Data) -> {address, Data};
coerce_chain_object2(contract, Data) -> {contract, Data};
coerce_chain_object2(signature, Data) -> Data.
decode_chain_object(Tag, S) ->
try
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of
{Tag, Data} -> {ok, Data};
{_, _} -> {error, wrong_prefix}
end
catch
error:missing_prefix -> {error, missing_prefix};
error:incorrect_size -> {error, incorrect_size}
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) ->
% 0 index since it represents a sophia list
coerce_list(Type, Elements, Direction, 0, [], []).
coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) ->
case coerce_direction(Type, Next, Direction) of
{ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({index, Index}, Errors),
coerce_list(Type, Rest, Direction, Index + 1, Good, [Wrapped | Broken])
end;
coerce_list(_Type, [], _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_list(_, [], _, _, _, Broken) ->
{error, combine_errors(Broken)}.
coerce_map(KeyType, ValType, Data, Direction) ->
coerce_map(KeyType, ValType, maps:iterator(Data), Direction, #{}, []).
coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) ->
case maps:next(Remaining) of
{K, V, RemainingAfter} ->
coerce_map2(KeyType, ValType, RemainingAfter, Direction, Good, Broken, K, V);
none ->
coerce_map_finish(Good, Broken)
end.
coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
case coerce_direction(KeyType, K, Direction) of
{ok, KFATE} ->
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE);
{error, Errors} ->
Wrapped = wrap_errors(map_key, Errors),
% Continue as if the key coerced successfully, so that we can give
% errors for both the key and the value.
coerce_map3(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken], K, V, error)
end.
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) ->
case coerce_direction(ValType, V, Direction) of
{ok, VFATE} ->
NewGood = Good#{KFATE => VFATE},
coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken);
{error, Errors} ->
Wrapped = wrap_errors({map_value, K}, Errors),
coerce_map(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken])
end.
coerce_map_finish(Good, []) ->
{ok, Good};
coerce_map_finish(_, Broken) ->
{error, combine_errors(Broken)}.
lookup_variant(Name, Variants) -> lookup_variant(Name, Variants, 0).
lookup_variant(Name, [{Name, Terms} | _], Tag) ->
{Tag, Terms};
lookup_variant(Name, [_ | Rest], Tag) ->
lookup_variant(Name, Rest, Tag + 1);
lookup_variant(_Name, [], _Tag) ->
not_found.
coerce_tuple(O, N, TermTypes, Terms, Direction) ->
case coerce_tuple_elements(TermTypes, Terms, Direction, tuple_element) of
{ok, Converted} ->
case Direction of
to_fate -> {ok, {tuple, list_to_tuple(Converted)}};
from_fate -> {ok, list_to_tuple(Converted)}
end;
{error, too_few_terms} ->
single_error({tuple_too_few_terms, O, N, list_to_tuple(Terms)});
{error, too_many_terms} ->
single_error({tuple_too_many_terms, O, N, list_to_tuple(Terms)});
Errors -> Errors
end.
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) ->
% FIXME: we could go through and add the variant tag to the adt_element
% paths?
case coerce_tuple_elements(TermTypes, Terms, Direction, adt_element) of
{ok, Converted} ->
case Direction of
to_fate ->
Arities = [length(VariantTerms)
|| {_, VariantTerms} <- Variants],
{ok, {variant, Arities, Tag, list_to_tuple(Converted)}};
from_fate ->
{ok, list_to_tuple([Name | Converted])}
end;
{error, too_few_terms} ->
single_error({adt_too_few_terms, O, N, Name, TermTypes, Terms});
{error, too_many_terms} ->
single_error({adt_too_many_terms, O, N, Name, TermTypes, Terms});
Errors -> Errors
end.
coerce_tuple_elements(Types, Terms, Direction, Tag) ->
% The sophia standard library uses 0 indexing for lists, and fst/snd/thd
% for tuples... Not sure how we should report errors in tuples, then.
coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []).
coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) ->
case coerce_direction(Type, Term, Direction) of
{ok, Value} ->
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({Tag, Index}, Errors),
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, Good, [Wrapped | Broken])
end;
coerce_tuple_elements([], [], _, _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_tuple_elements([], [], _, _, _, _, Broken) ->
{error, combine_errors(Broken)};
coerce_tuple_elements(_, [], _, _, _, _, _) ->
{error, too_few_terms};
coerce_tuple_elements([], _, _, _, _, _, _) ->
{error, too_many_terms}.
coerce_map_to_record(O, N, MemberTypes, Map) ->
case zip_record_fields(MemberTypes, Map) of
{ok, Zipped} ->
case coerce_zipped_bindings(Zipped, to_fate, field) of
{ok, [SingleElem]} ->
% Singleton records aren't implemented as FATE tuples at
% all.
{ok, SingleElem};
{ok, Converted} ->
{ok, {tuple, list_to_tuple(Converted)}};
Errors ->
Errors
end;
{error, {missing_fields, Missing}} ->
single_error({missing_fields, O, N, Missing});
{error, {unexpected_fields, Unexpected}} ->
Names = [Name || {Name, _} <- maps:to_list(Unexpected)],
single_error({unexpected_fields, O, N, Names})
end.
coerce_record_to_map(O, N, MemberTypes, Tuple) ->
{Names, Types} = lists:unzip(MemberTypes),
Terms = tuple_to_list(Tuple),
% FIXME: We could go through and change the record_element paths into field
% paths?
case coerce_tuple_elements(Types, Terms, from_fate, record_element) of
{ok, Converted} ->
Map = maps:from_list(lists:zip(Names, Converted)),
{ok, Map};
{error, too_few_terms} ->
single_error({record_too_few_terms, O, N, Tuple});
{error, too_many_terms} ->
single_error({record_too_many_terms, O, N, Tuple});
{error, Errors} ->
correct_record_error_paths(Names, Errors)
end.
correct_record_error_paths(Names, Errors) ->
CorrectOne = fun({Error, [{record_element, N} | Path]}) ->
FieldName = lists:nth(N + 1, Names),
{Error, [{record_element, N, FieldName} | Path]}
end,
Corrected = lists:map(CorrectOne, Errors),
{error, Corrected}.
zip_record_fields(Fields, Map) ->
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
{_, {_, Missing = [_|_]}} ->
{error, {missing_fields, lists:reverse(Missing)}};
{_, {Remaining, _}} when map_size(Remaining) > 0 ->
{error, {unexpected_fields, Remaining}};
{Zipped, _} ->
{ok, Zipped}
end.
zip_record_field({Name, Type}, {Remaining, Missing}) ->
case maps:take(Name, Remaining) of
{Term, RemainingAfter} ->
ZippedTerm = {{Name, Type}, Term},
{ZippedTerm, {RemainingAfter, Missing}};
error ->
{missing, {Remaining, [Name | Missing]}}
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).
-spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors}
when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(),
Erlang :: erlang_repr(),
Errors :: [{Reason, [PathStep]}],
Reason :: term(),
PathStep :: 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, [SingleMemberType]}}, Data) ->
% Singleton records aren't implemented as FATE tuples at all.
% Pretend they are, so we can get the full error indexing of the
% non-singletone case.
coerce_record_to_map(O, N, [SingleMemberType], {Data});
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(Type, Data) ->
TypeStr = type_to_iolist(Type),
io:format("Warning: Could not coerce term into ~s. Using term as is: ~p~n", [TypeStr, Data]),
{ok, Data}.
type_to_iolist({O, already_normalized, S}) ->
% Already normalized. Example output:
% type {map, [string, integer]}
opaque_type_to_iolist(O, S);
type_to_iolist({O, N, S}) ->
% Type alias. Print the alias, and then print the normalized version in
% parentheses. Example output:
% type "my_alias" (i.e. record type {"my_record_type", [integer]})
io_lib:format("type ~p (i.e. ~s)", [O, opaque_type_to_iolist(N, S)]).
opaque_type_to_iolist(N, {record, _}) ->
% N is the name of a record definition.
io_lib:format("record type ~p", [N]);
opaque_type_to_iolist(N, {variant, _}) ->
% N is the name of a variant definition.
io_lib:format("variant type ~p", [N]);
opaque_type_to_iolist(N, _) ->
% N is some other constructive type.
io_lib:format("type ~p", [N]).
%%% AACI Getters
-spec get_function_signature(AACI, Fun) -> {ok, Type} | {error, Reason}
when AACI :: aaci(),
Fun :: binary() | string(),
Type :: {term(), term()}, % FIXME
Reason :: bad_fun_name.
%% @doc
%% Look up the type information of a given function, in the AACI provided by
%% prepare_contract/1. This type information, particularly the return type, is
%% useful for calling decode_bytearray/2.
get_function_signature({aaci, _, FunDefs, _}, Fun) ->
case maps:find(Fun, FunDefs) of
{ok, A} -> {ok, A};
error -> {error, bad_fun_name}
end.
%%% Simple FATE/erlang tests
check_erlang_to_fate(Type, Sophia, Fate) ->
{ok, FateActual} = erlang_to_fate(Type, Sophia),
case FateActual of
Fate ->
ok;
_ ->
erlang:error({to_fate_failed, Fate, FateActual})
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
Sophia ->
ok;
_ ->
erlang:error({from_fate_failed, Sophia, SophiaActual})
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.
gmb_fate_encoding:serialize(Fate),
ok.
coerce_int_test() ->
{ok, Type} = annotate_type(integer, #{}),
check_roundtrip(Type, 123, 123).
coerce_address_test() ->
{ok, Type} = annotate_type(address, #{}),
check_roundtrip(Type,
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{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,
210,39,214>>}).
coerce_contract_test() ->
{ok, Type} = annotate_type(contract, #{}),
check_roundtrip(Type,
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{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,
210,39,214>>}).
coerce_signature_test() ->
{ok, Type} = annotate_type(signature, #{}),
check_roundtrip(Type,
"sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n",
<<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,
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>>).
coerce_signature_binary_test() ->
{ok, Type} = annotate_type(signature, #{}),
Binary = <<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,
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>>,
{ok, Binary} = erlang_to_fate(Type, {raw, Binary}),
{ok, Binary} = erlang_to_fate(Type, Binary),
ok.
coerce_bool_test() ->
{ok, Type} = annotate_type(boolean, #{}),
check_roundtrip(Type, true, true),
check_roundtrip(Type, false, false).
coerce_string_test() ->
{ok, Type} = annotate_type(string, #{}),
check_roundtrip(Type, "hello world", <<"hello world">>).
coerce_list_test() ->
{ok, Type} = annotate_type({list, [string]}, #{}),
check_roundtrip(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
coerce_map_test() ->
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}),
check_roundtrip(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
coerce_tuple_test() ->
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}),
check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
coerce_variant_test() ->
{ok, Type} = annotate_type({variant, [{"A", [integer]},
{"B", [integer, integer]}]},
#{}),
check_roundtrip(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
check_roundtrip(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
coerce_option_test() ->
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
check_roundtrip(Type, {"None"}, {variant, [0, 1], 0, {}}),
check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
coerce_bytes_test() ->
{ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}),
check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}).
coerce_bits_test() ->
{ok, Type} = annotate_type(bits, #{}),
check_roundtrip(Type, 5, {bits, 5}).
coerce_char_test() ->
{ok, Type} = annotate_type(char, #{}),
check_roundtrip(Type, $?, $?).
coerce_unicode_test() ->
{ok, Type} = annotate_type(char, #{}),
% Latin Small Letter C with cedilla and acute
{ok, $ḉ} = erlang_to_fate(Type, <<""/utf8>>),
ok.
coerce_hash_test() ->
{ok, Type} = annotate_type("hash", builtin_typedefs()),
Hash = list_to_binary(lists:seq(1,32)),
check_roundtrip(Type, Hash, Hash),
ok.
%%% Complex AACI paramter and namespace tests
aaci_from_string(String) ->
case so_compiler:from_string(String, [{aci, json}]) of
{ok, #{aci := ACI}} -> {ok, prepare(ACI)};
Error -> Error
end.
namespace_coerce_test() ->
Contract = "
namespace N =
record pair = { a : int, b : int }
contract C =
entrypoint f(): N.pair = { a = 1, b = 2 }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
record_substitution_test() ->
Contract = "
contract C =
record pair('t) = { a : 't, b : 't }
entrypoint f(): pair(int) = { a = 1, b = 2 }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
singleton_record_substitution_test() ->
Contract = "
contract C =
record single('t) = { it: 't }
entrypoint f(): single(int) = { it = 1 }
entrypoint g(): single(single(int)) = { it = { it = 2 } }
entrypoint h(): single(int * int) = { it = (3, 4) }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], FOutput}} = get_function_signature(AACI, "f"),
check_roundtrip(FOutput, #{"it" => 123}, 123),
{ok, {[], GOutput}} = get_function_signature(AACI, "g"),
check_roundtrip(GOutput, #{"it" => #{"it" => 123}}, 123),
{ok, {[], HOutput}} = get_function_signature(AACI, "h"),
check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}),
% Also check that records have accurate paths, since the implementation for
% record error paths is a bit fiddly.
{error, [{{tuple_too_many_terms, _, _, _}, [{record_element, 0, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}).
tuple_substitution_test() ->
Contract = "
contract C =
type triple('t1, 't2) = int * 't1 * 't2
entrypoint f(): triple(int, string) = (1, 2, \"hello\")
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
variant_substitution_test() ->
Contract = "
contract C =
datatype adt('a, 'b) = Left('a, 'b) | Right('b, int)
entrypoint f(): adt(string, int) = Left(\"hi\", 1)
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
check_roundtrip(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
nested_coerce_test() ->
Contract = "
contract C =
type pair('t) = 't * 't
record r = { f1 : pair(int), f2: pair(string) }
entrypoint f(): r = { f1 = (1, 2), f2 = (\"a\", \"b\") }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output,
#{ "f1" => {1, 2}, "f2" => {"a", "b"}},
{tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}).
state_coerce_test() ->
Contract = "
contract C =
type state = int
entrypoint init(): state = 0
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = get_function_signature(AACI, "init"),
check_roundtrip(Output, 0, 0).
param_test() ->
Contract = "
contract C =
type state = int
entrypoint init(x): state = x
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[{"x", Input}], Output}} = get_function_signature(AACI, "init"),
check_roundtrip(Input, 0, 0),
check_roundtrip(Output, 0, 0).
%%% Obscure Sophia types where we should check the AACI as well
obscure_aaci_test() ->
Contract = "
include \"Set.aes\"
contract C =
entrypoint options(): option(int) = None
entrypoint fixed_bytes(): bytes(4) = #DEADBEEF
entrypoint any_bytes(): bytes() = Bytes.to_any_size(#112233)
entrypoint bits(): bits = Bits.all
entrypoint character(): char = 'a'
entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF
entrypoint unit(): unit = ()
entrypoint ttl(x): Chain.ttl = FixedTTL(x)
entrypoint paying_for(x, y): Chain.paying_for_tx = Chain.PayingForTx(x, y)
entrypoint ga_meta_tx(x, y): Chain.ga_meta_tx = Chain.GAMetaTx(x, y)
entrypoint base_tx(x, y, z): Chain.base_tx = Chain.SpendTx(x, y, z)
entrypoint tx(a, b, c, d, e, f): Chain.tx =
{paying_for = a,
ga_metas = b,
actor = c,
fee = d,
ttl = e,
tx = f}
entrypoint pointee(x): AENS.pointee = AENS.AccountPt(x)
entrypoint name(x, y, z): AENS.name = AENS.Name(x, y, z)
entrypoint pointee2(x): AENSv2.pointee = AENSv2.DataPt(x)
entrypoint name2(x, y, z): AENSv2.name = AENSv2.Name(x, y, z)
entrypoint fr(x): MCL_BLS12_381.fr = x
entrypoint fp(x): MCL_BLS12_381.fp = x
entrypoint set(): Set.set(int) = Set.new()
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], {{bytes, [4]}, _, _}}} = get_function_signature(AACI, "fixed_bytes"),
{ok, {[], {{bytes, [any]}, _, _}}} = get_function_signature(AACI, "any_bytes"),
{ok, {[], {bits, _, _}}} = get_function_signature(AACI, "bits"),
{ok, {[], {char, _, _}}} = get_function_signature(AACI, "character"),
{ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = get_function_signature(AACI, "options"),
{ok, {[], {"hash", _, {bytes, [32]}}}} = get_function_signature(AACI, "hash"),
{ok, {[], {"unit", _, {tuple, []}}}} = get_function_signature(AACI, "unit"),
{ok, {_, {"Chain.ttl", _, {variant, _}}}} = get_function_signature(AACI, "ttl"),
{ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = get_function_signature(AACI, "paying_for"),
{ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = get_function_signature(AACI, "ga_meta_tx"),
{ok, {_, {"Chain.base_tx", _, {variant, _}}}} = get_function_signature(AACI, "base_tx"),
{ok, {_, {"Chain.tx", _, {record, _}}}} = get_function_signature(AACI, "tx"),
{ok, {_, {"AENS.pointee", _, {variant, _}}}} = get_function_signature(AACI, "pointee"),
{ok, {_, {"AENS.name", _, {variant, _}}}} = get_function_signature(AACI, "name"),
{ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = get_function_signature(AACI, "pointee2"),
{ok, {_, {"AENSv2.name", _, {variant, _}}}} = get_function_signature(AACI, "name2"),
{ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = get_function_signature(AACI, "fr"),
{ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = get_function_signature(AACI, "fp"),
{ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = get_function_signature(AACI, "set"),
ok.
name_coerce_test() ->
AddrSoph = "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
AddrFate = {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,
210,39,214>>},
{ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()),
TTLSoph = {"FixedTTL", 0},
TTLFate = {variant, [1, 1], 0, {0}},
check_roundtrip(TTL, TTLSoph, TTLFate),
{ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()),
PointeeSoph = {"AccountPt", AddrSoph},
PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}},
check_roundtrip(Pointee, PointeeSoph, PointeeFate),
{ok, Name} = annotate_type("AENS.name", builtin_typedefs()),
NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}},
NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}},
check_roundtrip(Name, NameSoph, NameFate).
void_coerce_test() ->
% Void itself can't be represented, but other types built out of void are
% valid.
{ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()),
check_roundtrip(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
{ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()),
check_roundtrip(NonList, [], []).