Any error reasons or paths are just term() still, and ACI doesn't have a defined spec in the compiler, so whatever, but the AACI types, the erlang representation of terms, and the four different kinds of coerce function are all spec'd now. Also some internal type substitution functions were given types, just in the hopes of catching some errors, but dyalizer doesn't seem to complain at all no matter how badly I break my code. Strange approach to making a type system, but oh well.
1270 lines
52 KiB
Erlang
1270 lines
52 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.8.2").
|
|
-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, 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});
|
|
Errors ->
|
|
Errors
|
|
end.
|
|
|
|
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, 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
|
|
|
|
-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}}).
|
|
|
|
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, [], []).
|
|
|