From d2163c1ff870521439c392e5d6df3c11fd9fa553 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Wed, 7 Jan 2026 09:40:55 +0000 Subject: [PATCH 1/3] split AACI out of hz.erl So far the interface to hz.erl is mostly unchanged, apart from prepare_aaci/1 Maybe prepare_aaci should be re-exported, but using it is exactly in line with the 'inconvenient but more flexible primitives' that hz_aaci.erl is meant to represent, so, maybe that is a fine place to have to go for it, dunno. --- src/hz.erl | 1092 +------------------------------------------- src/hz_aaci.erl | 1146 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1153 insertions(+), 1085 deletions(-) create mode 100644 src/hz_aaci.erl diff --git a/src/hz.erl b/src/hz.erl index 2d909e1..277f122 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -65,7 +65,6 @@ contract_create_built/8, contract_create/8, prepare_contract/1, - prepare_aaci/1, cache_aaci/2, lookup_aaci/1, aaci_lookup_spec/2, @@ -717,7 +716,7 @@ decode_bytearray_fate(EncodedStr) -> decode_bytearray(Type, EncodedStr) -> case decode_bytearray_fate(EncodedStr) of {ok, none} -> {ok, none}; - {ok, Object} -> coerce(Type, Object, from_fate); + {ok, Object} -> hz_aaci:coerce(Type, Object, from_fate); {error, Reason} -> {error, Reason} end. @@ -1099,7 +1098,7 @@ contract_create_built(CreatorID, Compiled, InitArgs) -> contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs) -> - AACI = prepare_aaci(maps:get(aci, Compiled)), + AACI = hz_aaci:prepare_aaci(maps:get(aci, Compiled)), case encode_call_data(AACI, "init", InitArgs) of {ok, CallData} -> assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, @@ -1438,779 +1437,10 @@ contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) -> prepare_contract(File) -> case so_compiler:file(File, [{aci, json}]) of - {ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)}; + {ok, #{aci := ACI}} -> {ok, hz_aaci:prepare_aaci(ACI)}; Error -> Error end. -prepare_aaci(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}. - -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}]). - -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). - -% 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]}. - -% 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]}} - }. - -% 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). - -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). - -coerce_bindings(VarTypes, Terms, Direction) -> - DefLength = length(VarTypes), - ArgLength = length(Terms), - if - DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg); - DefLength > ArgLength -> {error, too_few_args}; - DefLength < ArgLength -> {error, too_many_args} - end. - -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(Type, Term, Direction) of - {ok, NewTerm} -> - coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken); - {error, Errors} -> - Wrapped = wrap_errors({Tag, ArgName}, Errors), - coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken]) - end; -coerce_zipped_bindings([], _, _, Good, []) -> - {ok, lists:reverse(Good)}; -coerce_zipped_bindings([], _, _, _, Broken) -> - {error, combine_errors(Broken)}. - -wrap_errors(Location, Errors) -> - F = fun({Error, Path}) -> - {Error, [Location | Path]} - end, - lists:map(F, Errors). - -combine_errors(Broken) -> - F = fun(NextErrors, Acc) -> - NextErrors ++ Acc - end, - lists:foldl(F, [], Broken). - -coerce({_, _, integer}, S, _) when is_integer(S) -> - {ok, S}; -coerce({O, N, integer}, S, to_fate) when is_list(S) -> - try - Val = list_to_integer(S), - {ok, Val} - catch - error:badarg -> single_error({invalid, O, N, S}) - end; -coerce({O, N, address}, S, to_fate) -> - coerce_chain_object(O, N, address, account_pubkey, S); -coerce({_, _, address}, {address, Bin}, from_fate) -> - Address = gmser_api_encoder:encode(account_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; -coerce({O, N, contract}, S, to_fate) -> - coerce_chain_object(O, N, contract, contract_pubkey, S); -coerce({_, _, contract}, {contract, Bin}, from_fate) -> - Address = gmser_api_encoder:encode(contract_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; -coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) -> - % Usually to pass a binary in, you need to wrap it as {raw, Binary}, but - % 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}; -coerce({O, N, signature}, S, to_fate) -> - coerce_chain_object(O, N, signature, signature, S); -coerce({_, _, signature}, Bin, from_fate) -> - Address = gmser_api_encoder:encode(signature, Bin), - {ok, unicode:characters_to_list(Address)}; -%coerce({_, _, channel}, S, to_fate) when is_binary(S) -> - %{ok, {channel, S}}; -%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) -> - %{ok, S}; -coerce({_, _, boolean}, true, _) -> - {ok, true}; -coerce({_, _, boolean}, "true", _) -> - {ok, true}; -coerce({_, _, boolean}, false, _) -> - {ok, false}; -coerce({_, _, boolean}, "false", _) -> - {ok, false}; -coerce({O, N, boolean}, S, _) -> - single_error({invalid, O, N, S}); -coerce({O, N, string}, Str, Direction) -> - Result = case Direction of - to_fate -> unicode:characters_to_binary(Str); - from_fate -> unicode:characters_to_list(Str) - end, - case Result of - {error, _, _} -> - single_error({invalid, O, N, Str}); - {incomplete, _, _} -> - single_error({invalid, O, N, Str}); - StrBin -> - {ok, StrBin} - end; -coerce({_, _, char}, Val, _Direction) when is_integer(Val) -> - {ok, Val}; -coerce({O, N, char}, Str, to_fate) -> - 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; -coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) -> - coerce_bytes(O, N, Count, Bytes); -coerce({_, _, bits}, {bits, Num}, from_fate) -> - {ok, Num}; -coerce({_, _, bits}, Num, to_fate) when is_integer(Num) -> - {ok, {bits, Num}}; -coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) -> - Size = bit_size(Bits), - <> = Bits, - {ok, {bits, IntValue}}; -coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) -> - coerce_list(Type, Data, Direction); -coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, Direction); -coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) -> - ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, to_fate); -coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) -> - ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, from_fate); -coerce({O, N, {variant, Variants}}, Data, to_fate) 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; -coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) -> - coerce({O, N, {variant, Variants}}, {Name}, to_fate); -coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) -> - Terms = tuple_to_list(Tuple), - {Name, TermTypes} = lists:nth(Tag + 1, Variants), - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate); -coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) -> - coerce_map_to_record(O, N, MemberTypes, Map); -coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) -> - coerce_record_to_map(O, N, MemberTypes, Tuple); -coerce({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}; -coerce({O, N, _}, Data, from_fate) -> - case N of - already_normalized -> - io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]); - _ -> - io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data]) - end, - {ok, Data}; -coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}). - -coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> - single_error({partial_bytes, O, N, bit_size(Bytes)}); -coerce_bytes(_, _, any, Bytes) -> - {ok, Bytes}; -coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> - single_error({incorrect_size, O, N, Bytes}); -coerce_bytes(_, _, _, Bytes) -> - {ok, Bytes}. - -coerce_chain_object(_, _, _, _, {raw, Binary}) -> - {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_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(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(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(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. - -% Wraps a single error in a list, along with an empty path, so that other -% accumulating error handlers can work with it. -single_error(Reason) -> - {error, [{Reason, []}]}. - -coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> - % 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(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 = [Name || {Name, _} <- MemberTypes], - Types = [Type || {_, Type} <- 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. - - -spec cache_aaci(Label, AACI) -> ok when Label :: term(), AACI :: aaci(). @@ -2244,14 +1474,11 @@ lookup_aaci(Label) -> %% prepare_contract/1. This type information, particularly the return type, is %% useful for calling decode_bytearray/2. -aaci_lookup_spec({aaci, _, FunDefs, _}, Fun) -> - case maps:find(Fun, FunDefs) of - A = {ok, _} -> A; - error -> {error, bad_fun_name} - end; +aaci_lookup_spec(AACI = {aaci, _, _, _}, Fun) -> + hz_aaci:aaci_get_function_signature(AACI, Fun); aaci_lookup_spec({aaci, Label}, Fun) -> case hz_man:lookup_aaci(Label) of - {ok, AACI} -> aaci_lookup_spec(AACI, Fun); + {ok, AACI} -> hz_aaci:aaci_get_function_signature(AACI, Fun); error -> {error, aaci_not_found} end. @@ -2291,7 +1518,7 @@ encode_call_data({aaci, Label}, Fun, Args) -> end. encode_call_data2(ArgDef, Fun, Args) -> - case coerce_bindings(ArgDef, Args, to_fate) of + case hz_aaci:coerce_bindings(ArgDef, Args, to_fate) of {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); Errors -> Errors end. @@ -2612,308 +1839,3 @@ binary_sig_prefix() -> <<"Gajumaru Signed Binary:">>. % /v3/debug/token-supply/height/{height} % /v3/debug/crash - -%%% Simple coerce/3 tests - -% Round trip coerce run for the eunit tests below. If these results don't match -% then the test should fail. -try_coerce(Type, Sophia, Fate) -> - % Run both first, to see if they fail to produce any result. - {ok, FateActual} = coerce(Type, Sophia, to_fate), - {ok, SophiaActual} = coerce(Type, Fate, from_fate), - % Now check that the results were what we expected. - case FateActual of - Fate -> - ok; - _ -> - erlang:error({to_fate_failed, Fate, FateActual}) - end, - case SophiaActual of - Sophia -> - ok; - _ -> - erlang:error({from_fate_failed, Sophia, SophiaActual}) - end, - % 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, #{}), - try_coerce(Type, 123, 123). - -coerce_address_test() -> - {ok, Type} = annotate_type(address, #{}), - try_coerce(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, #{}), - try_coerce(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, #{}), - try_coerce(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} = coerce(Type, {raw, Binary}, to_fate), - {ok, Binary} = coerce(Type, Binary, to_fate), - ok. - -coerce_bool_test() -> - {ok, Type} = annotate_type(boolean, #{}), - try_coerce(Type, true, true), - try_coerce(Type, false, false). - -coerce_string_test() -> - {ok, Type} = annotate_type(string, #{}), - try_coerce(Type, "hello world", <<"hello world">>). - -coerce_list_test() -> - {ok, Type} = annotate_type({list, [string]}, #{}), - try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). - -coerce_map_test() -> - {ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}), - try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). - -coerce_tuple_test() -> - {ok, Type} = annotate_type({tuple, [integer, string]}, #{}), - try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}). - -coerce_variant_test() -> - {ok, Type} = annotate_type({variant, [{"A", [integer]}, - {"B", [integer, integer]}]}, - #{}), - try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), - try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). - -coerce_option_test() -> - {ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()), - try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}), - try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}). - -coerce_record_test() -> - {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), - try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). - -coerce_bytes_test() -> - {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), - try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). - -coerce_bits_test() -> - {ok, Type} = annotate_type(bits, #{}), - try_coerce(Type, 5, {bits, 5}). - -coerce_char_test() -> - {ok, Type} = annotate_type(char, #{}), - try_coerce(Type, $?, $?). - -coerce_unicode_test() -> - {ok, Type} = annotate_type(char, #{}), - % Latin Small Letter C with cedilla and acute - {ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate), - ok. - -coerce_hash_test() -> - {ok, Type} = annotate_type("hash", builtin_typedefs()), - Hash = list_to_binary(lists:seq(1,32)), - try_coerce(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_aaci(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}} = aaci_lookup_spec(AACI, "f"), - try_coerce(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}} = aaci_lookup_spec(AACI, "f"), - try_coerce(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}} = aaci_lookup_spec(AACI, "f"), - try_coerce(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}} = aaci_lookup_spec(AACI, "f"), - try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}), - try_coerce(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}} = aaci_lookup_spec(AACI, "f"), - try_coerce(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}} = aaci_lookup_spec(AACI, "init"), - try_coerce(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}} = aaci_lookup_spec(AACI, "init"), - try_coerce(Input, 0, 0), - try_coerce(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]}, _, _}}} = aaci_lookup_spec(AACI, "fixed_bytes"), - {ok, {[], {{bytes, [any]}, _, _}}} = aaci_lookup_spec(AACI, "any_bytes"), - {ok, {[], {bits, _, _}}} = aaci_lookup_spec(AACI, "bits"), - {ok, {[], {char, _, _}}} = aaci_lookup_spec(AACI, "character"), - - {ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = aaci_lookup_spec(AACI, "options"), - {ok, {[], {"hash", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "hash"), - {ok, {[], {"unit", _, {tuple, []}}}} = aaci_lookup_spec(AACI, "unit"), - - {ok, {_, {"Chain.ttl", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ttl"), - {ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "paying_for"), - {ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ga_meta_tx"), - {ok, {_, {"Chain.base_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "base_tx"), - {ok, {_, {"Chain.tx", _, {record, _}}}} = aaci_lookup_spec(AACI, "tx"), - - {ok, {_, {"AENS.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee"), - {ok, {_, {"AENS.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name"), - {ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee2"), - {ok, {_, {"AENSv2.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name2"), - - {ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "fr"), - {ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = aaci_lookup_spec(AACI, "fp"), - - {ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = aaci_lookup_spec(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}}, - try_coerce(TTL, TTLSoph, TTLFate), - {ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()), - PointeeSoph = {"AccountPt", AddrSoph}, - PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}}, - try_coerce(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}}}, - try_coerce(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()), - try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}), - {ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()), - try_coerce(NonList, [], []). - diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl new file mode 100644 index 0000000..0d56ef7 --- /dev/null +++ b/src/hz_aaci.erl @@ -0,0 +1,1146 @@ +%%% @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 "). +-copyright("Craig Everett "). +-license("GPL-3.0-or-later"). + +% Contract call and serialization interface functions +-export([prepare_contract/1, + prepare_aaci/1, + coerce/3, + coerce_bindings/3, + aaci_get_function_signature/2]). + +%%% Types + +-export_type([aaci/0]). + +-include_lib("eunit/include/eunit.hrl"). + +-type aaci() :: {aaci, term(), term(), term()}. + +%%% ACI/AACI + +-spec prepare_contract(File) -> {ok, AACI} | {error, Reason} + when File :: file:filename(), + AACI :: aaci(), + Reason :: term(). +%% @doc +%% Compile a contract and extract the function spec meta for use in future formation +%% of calldata + +prepare_contract(File) -> + case so_compiler:file(File, [{aci, json}]) of + {ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)}; + Error -> Error + end. + +prepare_aaci(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}. + +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}]). + +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 + +% 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]}. + +% 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). + +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). + +%%% Coerce + +coerce_bindings(VarTypes, Terms, Direction) -> + DefLength = length(VarTypes), + ArgLength = length(Terms), + if + DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg); + DefLength > ArgLength -> {error, too_few_args}; + DefLength < ArgLength -> {error, too_many_args} + end. + +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(Type, Term, Direction) of + {ok, NewTerm} -> + coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken); + {error, Errors} -> + Wrapped = wrap_errors({Tag, ArgName}, Errors), + coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken]) + end; +coerce_zipped_bindings([], _, _, Good, []) -> + {ok, lists:reverse(Good)}; +coerce_zipped_bindings([], _, _, _, Broken) -> + {error, combine_errors(Broken)}. + +wrap_errors(Location, Errors) -> + F = fun({Error, Path}) -> + {Error, [Location | Path]} + end, + lists:map(F, Errors). + +combine_errors(Broken) -> + F = fun(NextErrors, Acc) -> + NextErrors ++ Acc + end, + lists:foldl(F, [], Broken). + +coerce({_, _, integer}, S, _) when is_integer(S) -> + {ok, S}; +coerce({O, N, integer}, S, to_fate) when is_list(S) -> + try + Val = list_to_integer(S), + {ok, Val} + catch + error:badarg -> single_error({invalid, O, N, S}) + end; +coerce({O, N, address}, S, to_fate) -> + coerce_chain_object(O, N, address, account_pubkey, S); +coerce({_, _, address}, {address, Bin}, from_fate) -> + Address = gmser_api_encoder:encode(account_pubkey, Bin), + {ok, unicode:characters_to_list(Address)}; +coerce({O, N, contract}, S, to_fate) -> + coerce_chain_object(O, N, contract, contract_pubkey, S); +coerce({_, _, contract}, {contract, Bin}, from_fate) -> + Address = gmser_api_encoder:encode(contract_pubkey, Bin), + {ok, unicode:characters_to_list(Address)}; +coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) -> + % Usually to pass a binary in, you need to wrap it as {raw, Binary}, but + % 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}; +coerce({O, N, signature}, S, to_fate) -> + coerce_chain_object(O, N, signature, signature, S); +coerce({_, _, signature}, Bin, from_fate) -> + Address = gmser_api_encoder:encode(signature, Bin), + {ok, unicode:characters_to_list(Address)}; +%coerce({_, _, channel}, S, to_fate) when is_binary(S) -> + %{ok, {channel, S}}; +%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) -> + %{ok, S}; +coerce({_, _, boolean}, true, _) -> + {ok, true}; +coerce({_, _, boolean}, "true", _) -> + {ok, true}; +coerce({_, _, boolean}, false, _) -> + {ok, false}; +coerce({_, _, boolean}, "false", _) -> + {ok, false}; +coerce({O, N, boolean}, S, _) -> + single_error({invalid, O, N, S}); +coerce({O, N, string}, Str, Direction) -> + Result = case Direction of + to_fate -> unicode:characters_to_binary(Str); + from_fate -> unicode:characters_to_list(Str) + end, + case Result of + {error, _, _} -> + single_error({invalid, O, N, Str}); + {incomplete, _, _} -> + single_error({invalid, O, N, Str}); + StrBin -> + {ok, StrBin} + end; +coerce({_, _, char}, Val, _Direction) when is_integer(Val) -> + {ok, Val}; +coerce({O, N, char}, Str, to_fate) -> + 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; +coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) -> + coerce_bytes(O, N, Count, Bytes); +coerce({_, _, bits}, {bits, Num}, from_fate) -> + {ok, Num}; +coerce({_, _, bits}, Num, to_fate) when is_integer(Num) -> + {ok, {bits, Num}}; +coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) -> + Size = bit_size(Bits), + <> = Bits, + {ok, {bits, IntValue}}; +coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) -> + coerce_list(Type, Data, Direction); +coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) -> + coerce_map(KeyType, ValType, Data, Direction); +coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) -> + ElementList = tuple_to_list(Data), + coerce_tuple(O, N, ElementTypes, ElementList, to_fate); +coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) -> + ElementList = tuple_to_list(Data), + coerce_tuple(O, N, ElementTypes, ElementList, from_fate); +coerce({O, N, {variant, Variants}}, Data, to_fate) 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; +coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) -> + coerce({O, N, {variant, Variants}}, {Name}, to_fate); +coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) -> + Terms = tuple_to_list(Tuple), + {Name, TermTypes} = lists:nth(Tag + 1, Variants), + coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate); +coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) -> + coerce_map_to_record(O, N, MemberTypes, Map); +coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) -> + coerce_record_to_map(O, N, MemberTypes, Tuple); +coerce({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}; +coerce({O, N, _}, Data, from_fate) -> + case N of + already_normalized -> + io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]); + _ -> + io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data]) + end, + {ok, Data}; +coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}). + +coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> + single_error({partial_bytes, O, N, bit_size(Bytes)}); +coerce_bytes(_, _, any, Bytes) -> + {ok, Bytes}; +coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> + single_error({incorrect_size, O, N, Bytes}); +coerce_bytes(_, _, _, Bytes) -> + {ok, Bytes}. + +coerce_chain_object(_, _, _, _, {raw, Binary}) -> + {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_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(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(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(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. + +% Wraps a single error in a list, along with an empty path, so that other +% accumulating error handlers can work with it. +single_error(Reason) -> + {error, [{Reason, []}]}. + +coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> + % 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(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 = [Name || {Name, _} <- MemberTypes], + Types = [Type || {_, Type} <- 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. + + +%%% AACI Getters + +-spec aaci_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. + +aaci_get_function_signature({aaci, _, FunDefs, _}, Fun) -> + case maps:find(Fun, FunDefs) of + {ok, A} -> {ok, A}; + error -> {error, bad_fun_name} + end. + + +%%% Simple coerce/3 tests + +% Round trip coerce run for the eunit tests below. If these results don't match +% then the test should fail. +try_coerce(Type, Sophia, Fate) -> + % Run both first, to see if they fail to produce any result. + {ok, FateActual} = coerce(Type, Sophia, to_fate), + {ok, SophiaActual} = coerce(Type, Fate, from_fate), + % Now check that the results were what we expected. + case FateActual of + Fate -> + ok; + _ -> + erlang:error({to_fate_failed, Fate, FateActual}) + end, + case SophiaActual of + Sophia -> + ok; + _ -> + erlang:error({from_fate_failed, Sophia, SophiaActual}) + end, + % 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, #{}), + try_coerce(Type, 123, 123). + +coerce_address_test() -> + {ok, Type} = annotate_type(address, #{}), + try_coerce(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, #{}), + try_coerce(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, #{}), + try_coerce(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} = coerce(Type, {raw, Binary}, to_fate), + {ok, Binary} = coerce(Type, Binary, to_fate), + ok. + +coerce_bool_test() -> + {ok, Type} = annotate_type(boolean, #{}), + try_coerce(Type, true, true), + try_coerce(Type, false, false). + +coerce_string_test() -> + {ok, Type} = annotate_type(string, #{}), + try_coerce(Type, "hello world", <<"hello world">>). + +coerce_list_test() -> + {ok, Type} = annotate_type({list, [string]}, #{}), + try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). + +coerce_map_test() -> + {ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}), + try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). + +coerce_tuple_test() -> + {ok, Type} = annotate_type({tuple, [integer, string]}, #{}), + try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}). + +coerce_variant_test() -> + {ok, Type} = annotate_type({variant, [{"A", [integer]}, + {"B", [integer, integer]}]}, + #{}), + try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), + try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). + +coerce_option_test() -> + {ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()), + try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}), + try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}). + +coerce_record_test() -> + {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), + try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). + +coerce_bytes_test() -> + {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), + try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). + +coerce_bits_test() -> + {ok, Type} = annotate_type(bits, #{}), + try_coerce(Type, 5, {bits, 5}). + +coerce_char_test() -> + {ok, Type} = annotate_type(char, #{}), + try_coerce(Type, $?, $?). + +coerce_unicode_test() -> + {ok, Type} = annotate_type(char, #{}), + % Latin Small Letter C with cedilla and acute + {ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate), + ok. + +coerce_hash_test() -> + {ok, Type} = annotate_type("hash", builtin_typedefs()), + Hash = list_to_binary(lists:seq(1,32)), + try_coerce(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_aaci(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}} = aaci_get_function_signature(AACI, "f"), + try_coerce(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}} = aaci_get_function_signature(AACI, "f"), + try_coerce(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}} = aaci_get_function_signature(AACI, "f"), + try_coerce(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}} = aaci_get_function_signature(AACI, "f"), + try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}), + try_coerce(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}} = aaci_get_function_signature(AACI, "f"), + try_coerce(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}} = aaci_get_function_signature(AACI, "init"), + try_coerce(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}} = aaci_get_function_signature(AACI, "init"), + try_coerce(Input, 0, 0), + try_coerce(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]}, _, _}}} = aaci_get_function_signature(AACI, "fixed_bytes"), + {ok, {[], {{bytes, [any]}, _, _}}} = aaci_get_function_signature(AACI, "any_bytes"), + {ok, {[], {bits, _, _}}} = aaci_get_function_signature(AACI, "bits"), + {ok, {[], {char, _, _}}} = aaci_get_function_signature(AACI, "character"), + + {ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = aaci_get_function_signature(AACI, "options"), + {ok, {[], {"hash", _, {bytes, [32]}}}} = aaci_get_function_signature(AACI, "hash"), + {ok, {[], {"unit", _, {tuple, []}}}} = aaci_get_function_signature(AACI, "unit"), + + {ok, {_, {"Chain.ttl", _, {variant, _}}}} = aaci_get_function_signature(AACI, "ttl"), + {ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = aaci_get_function_signature(AACI, "paying_for"), + {ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = aaci_get_function_signature(AACI, "ga_meta_tx"), + {ok, {_, {"Chain.base_tx", _, {variant, _}}}} = aaci_get_function_signature(AACI, "base_tx"), + {ok, {_, {"Chain.tx", _, {record, _}}}} = aaci_get_function_signature(AACI, "tx"), + + {ok, {_, {"AENS.pointee", _, {variant, _}}}} = aaci_get_function_signature(AACI, "pointee"), + {ok, {_, {"AENS.name", _, {variant, _}}}} = aaci_get_function_signature(AACI, "name"), + {ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = aaci_get_function_signature(AACI, "pointee2"), + {ok, {_, {"AENSv2.name", _, {variant, _}}}} = aaci_get_function_signature(AACI, "name2"), + + {ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = aaci_get_function_signature(AACI, "fr"), + {ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = aaci_get_function_signature(AACI, "fp"), + + {ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = aaci_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}}, + try_coerce(TTL, TTLSoph, TTLFate), + {ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()), + PointeeSoph = {"AccountPt", AddrSoph}, + PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}}, + try_coerce(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}}}, + try_coerce(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()), + try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}), + {ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()), + try_coerce(NonList, [], []). + -- 2.30.2 From 3da9bd570babc6fcb77325bd58e4f1ecc9761755 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Fri, 9 Jan 2026 04:39:58 +0000 Subject: [PATCH 2/3] split coerce/3 into two functions Also renamed coerce_bindings to erlang_args_to_fate, to match. --- src/hz.erl | 4 +- src/hz_aaci.erl | 365 +++++++++++++++++++++++++++--------------------- 2 files changed, 204 insertions(+), 165 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index 277f122..c36dbc3 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -716,7 +716,7 @@ decode_bytearray_fate(EncodedStr) -> decode_bytearray(Type, EncodedStr) -> case decode_bytearray_fate(EncodedStr) of {ok, none} -> {ok, none}; - {ok, Object} -> hz_aaci:coerce(Type, Object, from_fate); + {ok, Object} -> hz_aaci:fate_to_erlang(Type, Object); {error, Reason} -> {error, Reason} end. @@ -1518,7 +1518,7 @@ encode_call_data({aaci, Label}, Fun, Args) -> end. encode_call_data2(ArgDef, Fun, Args) -> - case hz_aaci:coerce_bindings(ArgDef, Args, to_fate) of + case hz_aaci:erlang_args_to_fate(ArgDef, Args) of {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); Errors -> Errors end. diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 0d56ef7..2d21846 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -18,8 +18,9 @@ % Contract call and serialization interface functions -export([prepare_contract/1, prepare_aaci/1, - coerce/3, - coerce_bindings/3, + erlang_to_fate/2, + fate_to_erlang/2, + erlang_args_to_fate/2, aaci_get_function_signature/2]). %%% Types @@ -441,97 +442,51 @@ substitute_opaque_types(Bindings, Types) -> Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end, lists:map(Each, Types). -%%% Coerce +%%% Erlang to FATE -coerce_bindings(VarTypes, Terms, Direction) -> +erlang_args_to_fate(VarTypes, Terms) -> DefLength = length(VarTypes), ArgLength = length(Terms), if - DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg); + DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg); DefLength > ArgLength -> {error, too_few_args}; DefLength < ArgLength -> {error, too_many_args} end. -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(Type, Term, Direction) of - {ok, NewTerm} -> - coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken); - {error, Errors} -> - Wrapped = wrap_errors({Tag, ArgName}, Errors), - coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken]) - end; -coerce_zipped_bindings([], _, _, Good, []) -> - {ok, lists:reverse(Good)}; -coerce_zipped_bindings([], _, _, _, Broken) -> - {error, combine_errors(Broken)}. - -wrap_errors(Location, Errors) -> - F = fun({Error, Path}) -> - {Error, [Location | Path]} - end, - lists:map(F, Errors). - -combine_errors(Broken) -> - F = fun(NextErrors, Acc) -> - NextErrors ++ Acc - end, - lists:foldl(F, [], Broken). - -coerce({_, _, integer}, S, _) when is_integer(S) -> +erlang_to_fate({_, _, integer}, S) when is_integer(S) -> {ok, S}; -coerce({O, N, integer}, S, to_fate) when is_list(S) -> +erlang_to_fate({O, N, integer}, S) when is_list(S) -> try Val = list_to_integer(S), {ok, Val} catch error:badarg -> single_error({invalid, O, N, S}) end; -coerce({O, N, address}, S, to_fate) -> +erlang_to_fate({O, N, address}, S) -> coerce_chain_object(O, N, address, account_pubkey, S); -coerce({_, _, address}, {address, Bin}, from_fate) -> - Address = gmser_api_encoder:encode(account_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; -coerce({O, N, contract}, S, to_fate) -> +erlang_to_fate({O, N, contract}, S) -> coerce_chain_object(O, N, contract, contract_pubkey, S); -coerce({_, _, contract}, {contract, Bin}, from_fate) -> - Address = gmser_api_encoder:encode(contract_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; -coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) -> +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}; -coerce({O, N, signature}, S, to_fate) -> +erlang_to_fate({O, N, signature}, S) -> coerce_chain_object(O, N, signature, signature, S); -coerce({_, _, signature}, Bin, from_fate) -> - Address = gmser_api_encoder:encode(signature, Bin), - {ok, unicode:characters_to_list(Address)}; -%coerce({_, _, channel}, S, to_fate) when is_binary(S) -> +%erlang_to_fate({_, _, channel}, S) when is_binary(S) -> %{ok, {channel, S}}; -%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) -> - %{ok, S}; -coerce({_, _, boolean}, true, _) -> +erlang_to_fate({_, _, boolean}, true) -> {ok, true}; -coerce({_, _, boolean}, "true", _) -> +erlang_to_fate({_, _, boolean}, "true") -> {ok, true}; -coerce({_, _, boolean}, false, _) -> +erlang_to_fate({_, _, boolean}, false) -> {ok, false}; -coerce({_, _, boolean}, "false", _) -> +erlang_to_fate({_, _, boolean}, "false") -> {ok, false}; -coerce({O, N, boolean}, S, _) -> - single_error({invalid, O, N, S}); -coerce({O, N, string}, Str, Direction) -> - Result = case Direction of - to_fate -> unicode:characters_to_binary(Str); - from_fate -> unicode:characters_to_list(Str) - end, - case Result of +erlang_to_fate({O, N, string}, Str) -> + case unicode:characters_to_binary(Str) of {error, _, _} -> single_error({invalid, O, N, Str}); {incomplete, _, _} -> @@ -539,9 +494,9 @@ coerce({O, N, string}, Str, Direction) -> StrBin -> {ok, StrBin} end; -coerce({_, _, char}, Val, _Direction) when is_integer(Val) -> +erlang_to_fate({_, _, char}, Val) when is_integer(Val) -> {ok, Val}; -coerce({O, N, char}, Str, to_fate) -> +erlang_to_fate({O, N, char}, Str) -> Result = unicode:characters_to_list(Str), case Result of {error, _, _} -> @@ -553,27 +508,24 @@ coerce({O, N, char}, Str, to_fate) -> _ -> single_error({invalid, O, N, Str}) end; -coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) -> +erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) -> coerce_bytes(O, N, Count, Bytes); -coerce({_, _, bits}, {bits, Num}, from_fate) -> - {ok, Num}; -coerce({_, _, bits}, Num, to_fate) when is_integer(Num) -> +erlang_to_fate({_, _, bits}, Num) when is_integer(Num) -> {ok, {bits, Num}}; -coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) -> +erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) -> Size = bit_size(Bits), <> = Bits, {ok, {bits, IntValue}}; -coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) -> - coerce_list(Type, Data, Direction); -coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, Direction); -coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) -> +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); -coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) -> - ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, from_fate); -coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_size(Data) > 0 -> +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} -> @@ -582,17 +534,9 @@ coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_si ValidNames = [Valid || {Valid, _} <- Variants], single_error({invalid_variant, O, N, Name, ValidNames}) end; -coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) -> - coerce({O, N, {variant, Variants}}, {Name}, to_fate); -coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) -> - Terms = tuple_to_list(Tuple), - {Name, TermTypes} = lists:nth(Tag + 1, Variants), - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate); -coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) -> +erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) -> coerce_map_to_record(O, N, MemberTypes, Map); -coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) -> - coerce_record_to_map(O, N, MemberTypes, Tuple); -coerce({O, N, {unknown_type, _}}, Data, _) -> +erlang_to_fate({O, N, {unknown_type, _}}, Data) -> case N of already_normalized -> Message = "Warning: Unknown type ~p. Using term ~p as is.~n", @@ -602,24 +546,7 @@ coerce({O, N, {unknown_type, _}}, Data, _) -> io:format(Message, [O, N, Data]) end, {ok, Data}; -coerce({O, N, _}, Data, from_fate) -> - case N of - already_normalized -> - io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]); - _ -> - io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data]) - end, - {ok, Data}; -coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}). - -coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> - single_error({partial_bytes, O, N, bit_size(Bytes)}); -coerce_bytes(_, _, any, Bytes) -> - {ok, Bytes}; -coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> - single_error({incorrect_size, O, N, Bytes}); -coerce_bytes(_, _, _, Bytes) -> - {ok, Bytes}. +erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}). coerce_chain_object(_, _, _, _, {raw, Binary}) -> {ok, Binary}; @@ -644,12 +571,38 @@ decode_chain_object(Tag, S) -> 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(Type, Next, Direction) of + 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), @@ -672,7 +625,7 @@ coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) -> end. coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) -> - case coerce(KeyType, K, Direction) of + case coerce_direction(KeyType, K, Direction) of {ok, KFATE} -> coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE); {error, Errors} -> @@ -683,7 +636,7 @@ coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) -> end. coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) -> - case coerce(ValType, V, Direction) of + case coerce_direction(ValType, V, Direction) of {ok, VFATE} -> NewGood = Good#{KFATE => VFATE}, coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken); @@ -720,11 +673,6 @@ coerce_tuple(O, N, TermTypes, Terms, Direction) -> Errors -> Errors end. -% Wraps a single error in a list, along with an empty path, so that other -% accumulating error handlers can work with it. -single_error(Reason) -> - {error, [{Reason, []}]}. - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> % FIXME: we could go through and add the variant tag to the adt_element % paths? @@ -751,7 +699,7 @@ coerce_tuple_elements(Types, Terms, Direction, Tag) -> coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []). coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) -> - case coerce(Type, Term, Direction) of + case coerce_direction(Type, Term, Direction) of {ok, Value} -> coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken); {error, Errors} -> @@ -820,6 +768,91 @@ zip_record_field({Name, Type}, {Remaining, Missing}) -> {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). + +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 @@ -841,38 +874,44 @@ aaci_get_function_signature({aaci, _, FunDefs, _}, Fun) -> end. -%%% Simple coerce/3 tests +%%% Simple FATE/erlang tests -% Round trip coerce run for the eunit tests below. If these results don't match -% then the test should fail. -try_coerce(Type, Sophia, Fate) -> - % Run both first, to see if they fail to produce any result. - {ok, FateActual} = coerce(Type, Sophia, to_fate), - {ok, SophiaActual} = coerce(Type, Fate, from_fate), - % Now check that the results were what we expected. +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, + 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, + 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, #{}), - try_coerce(Type, 123, 123). + check_roundtrip(Type, 123, 123). coerce_address_test() -> {ok, Type} = annotate_type(address, #{}), - try_coerce(Type, + 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, @@ -880,7 +919,7 @@ coerce_address_test() -> coerce_contract_test() -> {ok, Type} = annotate_type(contract, #{}), - try_coerce(Type, + 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, @@ -888,7 +927,7 @@ coerce_contract_test() -> coerce_signature_test() -> {ok, Type} = annotate_type(signature, #{}), - try_coerce(Type, + 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, @@ -901,69 +940,69 @@ coerce_signature_binary_test() -> 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, 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} = coerce(Type, {raw, Binary}, to_fate), - {ok, Binary} = coerce(Type, Binary, to_fate), + {ok, Binary} = erlang_to_fate(Type, {raw, Binary}), + {ok, Binary} = erlang_to_fate(Type, Binary), ok. coerce_bool_test() -> {ok, Type} = annotate_type(boolean, #{}), - try_coerce(Type, true, true), - try_coerce(Type, false, false). + check_roundtrip(Type, true, true), + check_roundtrip(Type, false, false). coerce_string_test() -> {ok, Type} = annotate_type(string, #{}), - try_coerce(Type, "hello world", <<"hello world">>). + check_roundtrip(Type, "hello world", <<"hello world">>). coerce_list_test() -> {ok, Type} = annotate_type({list, [string]}, #{}), - try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). + check_roundtrip(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). coerce_map_test() -> {ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}), - try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). + check_roundtrip(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). coerce_tuple_test() -> {ok, Type} = annotate_type({tuple, [integer, string]}, #{}), - try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}). + check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}). coerce_variant_test() -> {ok, Type} = annotate_type({variant, [{"A", [integer]}, {"B", [integer, integer]}]}, #{}), - try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), - try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). + 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()), - try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}), - try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}). + 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}]}, #{}), - try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). + check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). coerce_bytes_test() -> {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), - try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). + check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). coerce_bits_test() -> {ok, Type} = annotate_type(bits, #{}), - try_coerce(Type, 5, {bits, 5}). + check_roundtrip(Type, 5, {bits, 5}). coerce_char_test() -> {ok, Type} = annotate_type(char, #{}), - try_coerce(Type, $?, $?). + check_roundtrip(Type, $?, $?). coerce_unicode_test() -> {ok, Type} = annotate_type(char, #{}), % Latin Small Letter C with cedilla and acute - {ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate), + {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)), - try_coerce(Type, Hash, Hash), + check_roundtrip(Type, Hash, Hash), ok. @@ -985,7 +1024,7 @@ namespace_coerce_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), - try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). + check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). record_substitution_test() -> Contract = " @@ -995,7 +1034,7 @@ record_substitution_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), - try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). + check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). tuple_substitution_test() -> Contract = " @@ -1005,7 +1044,7 @@ tuple_substitution_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), - try_coerce(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}). + check_roundtrip(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}). variant_substitution_test() -> Contract = " @@ -1015,8 +1054,8 @@ variant_substitution_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), - try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}), - try_coerce(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}). + check_roundtrip(Output, {"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 = " @@ -1027,7 +1066,7 @@ nested_coerce_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "f"), - try_coerce(Output, + check_roundtrip(Output, #{ "f1" => {1, 2}, "f2" => {"a", "b"}}, {tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}). @@ -1039,7 +1078,7 @@ state_coerce_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[], Output}} = aaci_get_function_signature(AACI, "init"), - try_coerce(Output, 0, 0). + check_roundtrip(Output, 0, 0). param_test() -> Contract = " @@ -1049,8 +1088,8 @@ param_test() -> ", {ok, AACI} = aaci_from_string(Contract), {ok, {[{"x", Input}], Output}} = aaci_get_function_signature(AACI, "init"), - try_coerce(Input, 0, 0), - try_coerce(Output, 0, 0). + check_roundtrip(Input, 0, 0), + check_roundtrip(Output, 0, 0). %%% Obscure Sophia types where we should check the AACI as well @@ -1126,21 +1165,21 @@ name_coerce_test() -> {ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()), TTLSoph = {"FixedTTL", 0}, TTLFate = {variant, [1, 1], 0, {0}}, - try_coerce(TTL, TTLSoph, TTLFate), + check_roundtrip(TTL, TTLSoph, TTLFate), {ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()), PointeeSoph = {"AccountPt", AddrSoph}, PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}}, - try_coerce(Pointee, PointeeSoph, PointeeFate), + check_roundtrip(Pointee, PointeeSoph, PointeeFate), {ok, Name} = annotate_type("AENS.name", builtin_typedefs()), NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}}, NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}}, - try_coerce(Name, NameSoph, NameFate). + 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()), - try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}), + check_roundtrip(NonOption, {"None"}, {variant, [0, 1], 0, {}}), {ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()), - try_coerce(NonList, [], []). + check_roundtrip(NonList, [], []). -- 2.30.2 From 4f1958b21046595a49f60e95793f6adca4f8b444 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Tue, 13 Jan 2026 01:19:29 +0000 Subject: [PATCH 3/3] use lists:unzip/1 Just a little thing I noticed could be improved. --- src/hz_aaci.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 2d21846..8e10795 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -732,8 +732,7 @@ coerce_map_to_record(O, N, MemberTypes, Map) -> end. coerce_record_to_map(O, N, MemberTypes, Tuple) -> - Names = [Name || {Name, _} <- MemberTypes], - Types = [Type || {_, Type} <- MemberTypes], + {Names, Types} = lists:unzip(MemberTypes), Terms = tuple_to_list(Tuple), % FIXME: We could go through and change the record_element paths into field % paths? -- 2.30.2