diff --git a/src/hz.erl b/src/hz.erl index 0001cef..fda0311 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -77,6 +77,7 @@ -export_type([chain_node/0, network_id/0, chain_error/0]). +-include_lib("eunit/include/eunit.hrl"). -type chain_node() :: {inet:ip_address(), inet:port_number()}. -type network_id() :: string(). @@ -1404,95 +1405,100 @@ prepare_contract(File) -> end. prepare_aaci(ACI) -> - % NOTE this will also pick up the main contract; as a result the main - % contract extraction later on shouldn't bother with typedefs. - Contracts = [ContractDef || #{contract := ContractDef} <- ACI], - Types = simplify_contract_types(Contracts, #{}), + % 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. + Specs = annotate_function_specs(OpaqueSpecs, TypeDefs, #{}), + + {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), - Specs = simplify_specs(SpecDefs, #{}, Types), - {aaci, Name, Specs, Types}. + % Turn these specifications into opaque types that we can reason about. + Specs = lists:map(fun convert_function_spec/1, SpecDefs), -simplify_contract_types([], Types) -> - Types; -simplify_contract_types([Next | Rest], Types) -> - TypeDefs = maps:get(typedefs, Next), - NameBin = maps:get(name, Next), + % 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), - Types2 = maps:put(Name, {[], contract}, Types), - Types3 = case maps:find(state, Next) of - {ok, StateDefACI} -> - StateDefOpaque = opaque_type([], StateDefACI), - maps:put(Name ++ ".state", {[], StateDefOpaque}, Types2); - error -> - Types2 - end, - Types4 = simplify_typedefs(TypeDefs, Types3, Name ++ "."), - simplify_contract_types(Rest, Types4). + ArgTypes = lists:map(fun convert_arg/1, Args), + ResultType = opaque_type([], Result), + {Name, ArgTypes, ResultType}. -simplify_typedefs([], Types, _NamePrefix) -> - Types; -simplify_typedefs([Next | Rest], Types, NamePrefix) -> - #{name := NameBin, vars := ParamDefs, typedef := T} = Next, - Name = NamePrefix ++ binary_to_list(NameBin), - Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs], - Type = opaque_type(Params, T), - NewTypes = maps:put(Name, {Params, Type}, Types), - simplify_typedefs(Rest, NewTypes, NamePrefix). - -simplify_specs([], Specs, _Types) -> - Specs; -simplify_specs([Next | Rest], Specs, Types) -> - #{name := NameBin, arguments := ArgDefs, returns := ResultDef} = Next, +convert_arg(#{name := NameBin, type := TypeDef}) -> Name = binary_to_list(NameBin), - ArgTypes = [simplify_args(Arg, Types) || Arg <- ArgDefs], - {ok, ResultType} = type(ResultDef, Types), - NewSpecs = maps:put(Name, {ArgTypes, ResultType}, Specs), - simplify_specs(Rest, NewSpecs, Types). - -simplify_args(#{name := NameBin, type := TypeDef}, Types) -> - Name = binary_to_list(NameBin), - % FIXME We should make this error more informative, and continue - % propogating it up, so that the user can provide their own ACI and find - % out whether it worked or not. At that point ACI -> AACI could almost be a - % module or package of its own. - {ok, Type} = type(TypeDef, Types), + {ok, Type} = opaque_type([], TypeDef), {Name, 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 flattened -% 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 flattened 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. +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]. -type(T, Types) -> - O = opaque_type([], T), - flatten_opaque_type(O, Types). +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 @@ -1516,7 +1522,7 @@ 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 builtins, lists for user defined types +% atoms for builtins, strings (lists) for user-defined types opaque_type_name(<<"int">>) -> integer; opaque_type_name(<<"address">>) -> address; opaque_type_name(<<"contract">>) -> contract; @@ -1527,16 +1533,49 @@ opaque_type_name(<<"map">>) -> map; opaque_type_name(<<"string">>) -> string; opaque_type_name(Name) -> binary_to_list(Name). -flatten_opaque_type(T, Types) -> +% 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_types(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} -> - flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types); + annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types); Error -> Error end. -flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) -> - case flatten_normalized_type(NExpanded, Types) of +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}}; @@ -1546,48 +1585,48 @@ flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) -> Error end. -flatten_opaque_types([T | Rest], Types, Acc) -> - case flatten_opaque_type(T, Types) of - {ok, Type} -> flatten_opaque_types(Rest, Types, [Type | Acc]); +annotate_types([T | Rest], Types, Acc) -> + case annotate_type(T, Types) of + {ok, Type} -> annotate_types(Rest, Types, [Type | Acc]); Error -> Error end; -flatten_opaque_types([], _Types, Acc) -> +annotate_types([], _Types, Acc) -> {ok, lists:reverse(Acc)}. -flatten_opaque_bindings([{Name, T} | Rest], Types, Acc) -> - case flatten_opaque_type(T, Types) of - {ok, Type} -> flatten_opaque_bindings(Rest, Types, [{Name, Type} | Acc]); - Error -> Error - end; -flatten_opaque_bindings([], _Types, Acc) -> - {ok, lists:reverse(Acc)}. - -flatten_opaque_variants([{Name, Elems} | Rest], Types, Acc) -> - case flatten_opaque_types(Elems, Types, []) of - {ok, ElemsFlat} -> flatten_opaque_variants(Rest, Types, [{Name, ElemsFlat} | Acc]); - Error -> Error - end; -flatten_opaque_variants([], _Types, Acc) -> - {ok, lists:reverse(Acc)}. - -flatten_normalized_type(PrimitiveType, _Types) when is_atom(PrimitiveType) -> +annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) -> {ok, PrimitiveType}; -flatten_normalized_type({variant, VariantsOpaque}, Types) -> - case flatten_opaque_variants(VariantsOpaque, Types, []) of +annotate_type_subexpressions({variant, VariantsOpaque}, Types) -> + case annotate_variants(VariantsOpaque, Types, []) of {ok, Variants} -> {ok, {variant, Variants}}; Error -> Error end; -flatten_normalized_type({record, FieldsOpaque}, Types) -> - case flatten_opaque_bindings(FieldsOpaque, Types, []) of +annotate_type_subexpressions({record, FieldsOpaque}, Types) -> + case annotate_bindings(FieldsOpaque, Types, []) of {ok, Fields} -> {ok, {record, Fields}}; Error -> Error end; -flatten_normalized_type({T, ElemsOpaque}, Types) -> - case flatten_opaque_types(ElemsOpaque, Types, []) of +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)}. + normalize_opaque_type(T, Types) -> case type_is_expanded(T) of false -> normalize_opaque_type(T, Types, true); @@ -1657,12 +1696,39 @@ substitute_opaque_type(Bindings, {var, VarName}) -> false -> {error, invalid_aci}; {_, TypeArg} -> {ok, TypeArg} end; +substitute_opaque_type(Bindings, {variant, Args}) -> + case substitute_variant_types(Bindings, Args, []) of + {ok, Result} -> {ok, {variant, Result}}; + Error -> Error + end; +substitute_opaque_type(Bindings, {record, Args}) -> + case substitute_record_types(Bindings, Args, []) of + {ok, Result} -> {ok, {record, Result}}; + Error -> Error + end; substitute_opaque_type(Bindings, {Connective, Args}) -> case substitute_opaque_types(Bindings, Args, []) of {ok, Result} -> {ok, {Connective, Result}}; Error -> Error end; -substitute_opaque_type(_Bindings, Type) -> {ok, Type}. +substitute_opaque_type(_Bindings, Type) -> + {ok, Type}. + +substitute_variant_types(Bindings, [{VariantName, Elements} | Rest], Acc) -> + case substitute_opaque_types(Bindings, Elements, []) of + {ok, Result} -> substitute_variant_types(Bindings, Rest, [{VariantName, Result} | Acc]); + Error -> Error + end; +substitute_variant_types(_Bindings, [], Acc) -> + {ok, lists:reverse(Acc)}. + +substitute_record_types(Bindings, [{ElementName, Type} | Rest], Acc) -> + case substitute_opaque_type(Bindings, Type) of + {ok, Result} -> substitute_record_types(Bindings, Rest, [{ElementName, Result} | Acc]); + Error -> Error + end; +substitute_record_types(_Bindings, [], Acc) -> + {ok, lists:reverse(Acc)}. substitute_opaque_types(Bindings, [Next | Rest], Acc) -> case substitute_opaque_type(Bindings, Next) of @@ -2140,3 +2206,155 @@ eu(N, Size) -> % /v3/debug/check-tx/pool/{hash} % /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, + 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_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_record_test() -> + {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), + try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). + + +%%% 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). +