From cc3e32217979a91085837250ea4c74d2bd2aaac3 Mon Sep 17 00:00:00 2001 From: Ulf Norell Date: Tue, 26 Feb 2019 14:34:57 +0100 Subject: [PATCH 1/6] new version of to_sophia_value takes function name and binary blob --- src/aeso_compiler.erl | 72 +++++++++++++++++++++++------------------ test/aeso_abi_tests.erl | 8 ++--- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 7aaa77a..5a841d8 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -15,8 +15,8 @@ , create_calldata/3 , version/0 , sophia_type_to_typerep/1 - , to_sophia_value/2 - , to_sophia_value/3 + , to_sophia_value/4 + , to_sophia_value/5 ]). -include_lib("aebytecode/include/aeb_opcodes.hrl"). @@ -145,40 +145,53 @@ check_call(ContractString, Options) -> fun (E) -> io_lib:format("~p", [E]) end)} end. --spec to_sophia_value(string(), aeso_sophia:data()) -> +-spec to_sophia_value(string(), string(), ok | error | revert, aeso_sophia:data()) -> {ok, aeso_syntax:expr()} | {error, term()}. -to_sophia_value(ContractString, Data) -> - to_sophia_value(ContractString, Data, []). +to_sophia_value(ContractString, Fun, ResType, Data) -> + to_sophia_value(ContractString, Fun, ResType, Data, []). --spec to_sophia_value(string(), aeso_sophia:data(), options()) -> +-spec to_sophia_value(string(), string(), ok | error | revert, binary(), options()) -> {ok, aeso_syntax:expr()} | {error, term()}. -to_sophia_value(ContractString, Data, Options) -> +to_sophia_value(_, _, error, Err, _Options) -> + {ok, {app, [], {id, [], "error"}, [{string, [], Err}]}}; +to_sophia_value(_, _, revert, Data, _Options) -> + case aeso_heap:from_binary(string, Data) of + {ok, Err} -> {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}}; + {error, _} = Err -> Err + end; +to_sophia_value(ContractString, FunName, ok, Data, Options) -> try Ast = parse(ContractString, Options), ok = pp_sophia_code(Ast, Options), ok = pp_ast(Ast, Options), {Env, TypedAst} = aeso_ast_infer_types:infer(Ast, [return_env]), - {ok, Type0} = get_decode_type(TypedAst), - Type = aeso_ast_infer_types:unfold_types_in_type(Env, Type0, [unfold_record_types, unfold_variant_types]), ok = pp_typed_ast(TypedAst, Options), + {ok, Type0} = get_decode_type(FunName, TypedAst), + Type = aeso_ast_infer_types:unfold_types_in_type(Env, Type0, [unfold_record_types, unfold_variant_types]), Icode = to_icode(TypedAst, Options), - {ok, VmType} = get_decode_vm_type(Icode), + VmType = aeso_ast_to_icode:ast_typerep(Type, Icode), ok = pp_icode(Icode, Options), - try - {ok, translate_vm_value(VmType, Type, Data)} - catch throw:cannot_translate_to_sophia -> - Type0Str = prettypr:format(aeso_pretty:type(Type0)), - {error, join_errors("Translation error", [io_lib:format("Cannot translate VM value ~p\n of type ~p\n to Sophia type ~s\n", - [Data, VmType, Type0Str])], - fun (E) -> E end)} + case aeso_heap:from_binary(VmType, Data) of + {ok, VmValue} -> + try + {ok, translate_vm_value(VmType, Type, VmValue)} + catch throw:cannot_translate_to_sophia -> + Type0Str = prettypr:format(aeso_pretty:type(Type0)), + {error, join_errors("Translation error", [lists:flatten(io_lib:format("Cannot translate VM value ~p\n of type ~p\n to Sophia type ~s\n", + [Data, VmType, Type0Str]))], + fun (E) -> E end)} + end; + {error, _Err} -> + {error, join_errors("Decode errors", [lists:flatten(io_lib:format("Failed to decode binary at type ~p", [VmType]))], + fun(E) -> E end)} end catch error:{parse_errors, Errors} -> {error, join_errors("Parse errors", Errors, fun (E) -> E end)}; error:{type_errors, Errors} -> {error, join_errors("Type errors", Errors, fun (E) -> E end)}; - error:{badmatch, {error, missing_decode_function}} -> - {error, join_errors("Type errors", ["missing __decode function"], + error:{badmatch, {error, missing_function}} -> + {error, join_errors("Type errors", ["no function: '" ++ FunName ++ "'"], fun (E) -> E end)}; throw:Error -> %Don't ask {error, join_errors("Code errors", [Error], @@ -286,21 +299,18 @@ get_call_type([_ | Contracts]) -> %% The __call should be in the final contract get_call_type(Contracts). -get_decode_type([{contract, _, _, Defs}]) -> - case [ DecodeType - || {letfun, _, {id, _, ?DECODE_NAME}, [{arg, _, _, DecodeType}], _Ret, _} <- Defs ] of +get_decode_type(FunName, [{contract, _, _, Defs}]) -> + GetType = fun({letfun, _, {id, _, Name}, _, Ret, _}) when Name == FunName -> [Ret]; + ({fun_decl, _, {id, _, Name}, {fun_t, _, _, _, Ret}}) when Name == FunName -> [Ret]; + (_) -> [] end, + io:format("~p\n", [Defs]), + case lists:flatmap(GetType, Defs) of [Type] -> {ok, Type}; - [] -> {error, missing_call_function} + [] -> {error, missing_function} end; -get_decode_type([_ | Contracts]) -> +get_decode_type(FunName, [_ | Contracts]) -> %% The __decode should be in the final contract - get_decode_type(Contracts). - -get_decode_vm_type(#{ functions := Funs }) -> - case [ VMType || {[_, ?DECODE_NAME], _, [{_, VMType}], _, _} <- Funs ] of - [Type] -> {ok, Type}; - [] -> {error, missing_decode_function} - end. + get_decode_type(FunName, Contracts). %% Translate an icode value (error if not value) to an Erlang term that can be %% consumed by aeso_heap:to_binary(). diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index d5d2538..ae4979d 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -76,17 +76,13 @@ encode_decode_sophia_string(SophiaType, String) -> Code = [ "contract MakeCall =\n" , " type arg_type = ", SophiaType, "\n" , TypeDefs - , " function foo : arg_type => _\n" + , " function foo : arg_type => arg_type\n" , " function __call() = foo(", String, ")\n" ], case aeso_compiler:check_call(lists:flatten(Code), []) of {ok, _, {[Type], _}, [Arg]} -> io:format("Type ~p~n", [Type]), Data = encode(Arg), - Decoded = decode(Type, Data), - DecodeCode = [ "contract Decode =\n", - TypeDefs, - " function __decode(_ : ", SophiaType, ") = ()\n" ], - case aeso_compiler:to_sophia_value(DecodeCode, Decoded) of + case aeso_compiler:to_sophia_value(Code, "foo", ok, Data) of {ok, Sophia} -> lists:flatten(io_lib:format("~s", [prettypr:format(aeso_pretty:expr(Sophia))])); {error, Err} -> From 5a4a84805f07f47f7fd865a221d2844c4f94854a Mon Sep 17 00:00:00 2001 From: Ulf Norell Date: Tue, 26 Feb 2019 15:01:27 +0100 Subject: [PATCH 2/6] change create_calldata function to also take fun name and arguments --- src/aeso_compiler.erl | 51 +++++++++++++++++++++-------------------- test/aeso_abi_tests.erl | 12 ++++------ 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 5a841d8..dbdd5a0 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -11,8 +11,8 @@ -export([ file/1 , file/2 , from_string/2 - , check_call/2 - , create_calldata/3 + , check_call/4 + , create_calldata/4 , version/0 , sophia_type_to_typerep/1 , to_sophia_value/4 @@ -110,10 +110,11 @@ join_errors(Prefix, Errors, Pfun) -> %% function (foo, say) and a function __call() = foo(args) calling this %% function. Returns the name of the called functions, typereps and Erlang %% terms for the arguments. --spec check_call(string(), options()) -> {ok, string(), {[Type], Type | any}, [term()]} | {error, term()} +-spec check_call(string(), string(), [string()], options()) -> {ok, string(), {[Type], Type | any}, [term()]} | {error, term()} when Type :: term(). -check_call(ContractString, Options) -> +check_call(ContractString0, FunName, Args, Options) -> try + ContractString = insert_call_function(ContractString0, FunName, Args, Options), Ast = parse(ContractString, Options), ok = pp_sophia_code(Ast, Options), ok = pp_ast(Ast, Options), @@ -145,6 +146,24 @@ check_call(ContractString, Options) -> fun (E) -> io_lib:format("~p", [E]) end)} end. +%% Add the __call function to a contract. +-spec insert_call_function(string(), string(), [string()], options()) -> string(). +insert_call_function(Code, FunName, Args, Options) -> + Ast = parse(Code, Options), + Ind = last_contract_indent(Ast), + lists:flatten( + [ Code, + "\n\n", + lists:duplicate(Ind, " "), + "function __call() = ", FunName, "(", string:join(Args, ","), ")\n" + ]). + +last_contract_indent(Decls) -> + case lists:last(Decls) of + {_, _, _, [Decl | _]} -> aeso_syntax:get_ann(col, Decl, 1) - 1; + _ -> 0 + end. + -spec to_sophia_value(string(), string(), ok | error | revert, aeso_sophia:data()) -> {ok, aeso_syntax:expr()} | {error, term()}. to_sophia_value(ContractString, Fun, ResType, Data) -> @@ -252,34 +271,16 @@ translate_vm_value(VmTypes, {constr_t, _, Con, Types}, Args) translate_vm_value(_VmType, _Type, _Data) -> throw(cannot_translate_to_sophia). --spec create_calldata(map(), string(), string()) -> +-spec create_calldata(map(), string(), string(), [string()]) -> {ok, binary(), aeso_sophia:type(), aeso_sophia:type()} | {error, argument_syntax_error}. -create_calldata(Contract, "", CallCode) when is_map(Contract) -> - case check_call(CallCode, []) of +create_calldata(Contract, Code, Fun, Args) when is_map(Contract) -> + case check_call(Code, Fun, Args, []) of {ok, FunName, {ArgTypes, RetType}, Args} -> aeso_abi:create_calldata(Contract, FunName, Args, ArgTypes, RetType); {error, _} = Err -> Err - end; -create_calldata(Contract, Function, Argument) when is_map(Contract) -> - %% Slightly hacky shortcut to let you get away without writing the full - %% call contract code. - %% Function should be "foo : type", and - %% Argument should be "Arg1, Arg2, .., ArgN" (no parens) - case string:lexemes(Function, ": ") of - %% If function is a single word fallback to old calldata generation - [FunName] -> aeso_abi:old_create_calldata(Contract, FunName, Argument); - [FunName | _] -> - Args = lists:map(fun($\n) -> 32; (X) -> X end, Argument), %% newline to space - CallContract = lists:flatten( - [ "contract MakeCall =\n" - , " function ", Function, "\n" - , " function __call() = ", FunName, "(", Args, ")" - ]), - create_calldata(Contract, "", CallContract) end. - get_arg_icode(Funs) -> case [ Args || {[_, ?CALL_NAME], _, _, {funcall, _, Args}, _} <- Funs ] of [Args] -> Args; diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index ae4979d..8557975 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -70,15 +70,13 @@ encode_decode_sophia_test() -> encode_decode_sophia_string(SophiaType, String) -> io:format("String ~p~n", [String]), - TypeDefs = [" type an_alias('a) = (string, 'a)\n", - " record r = {x : an_alias(int), y : variant}\n" - " datatype variant = Red | Blue(map(string, int))\n"], Code = [ "contract MakeCall =\n" , " type arg_type = ", SophiaType, "\n" - , TypeDefs - , " function foo : arg_type => arg_type\n" - , " function __call() = foo(", String, ")\n" ], - case aeso_compiler:check_call(lists:flatten(Code), []) of + , " type an_alias('a) = (string, 'a)\n" + , " record r = {x : an_alias(int), y : variant}\n" + , " datatype variant = Red | Blue(map(string, int))\n" + , " function foo : arg_type => arg_type\n" ], + case aeso_compiler:check_call(lists:flatten(Code), "foo", [String], []) of {ok, _, {[Type], _}, [Arg]} -> io:format("Type ~p~n", [Type]), Data = encode(Arg), From e0fff00e6428afcc4fec01f3d6aa03ee311cf9e6 Mon Sep 17 00:00:00 2001 From: Ulf Norell Date: Tue, 26 Feb 2019 17:31:53 +0100 Subject: [PATCH 3/6] get rid of byte code argument to create_calldata This means that there is less type checking at calldata creation time. Make sure that we check that the function hash exists before calling a contract! --- src/aeso_abi.erl | 43 ++++++------------------------------------- src/aeso_compiler.erl | 10 +++++----- 2 files changed, 11 insertions(+), 42 deletions(-) diff --git a/src/aeso_abi.erl b/src/aeso_abi.erl index c9f32ba..cea7c45 100644 --- a/src/aeso_abi.erl +++ b/src/aeso_abi.erl @@ -11,7 +11,7 @@ -define(HASH_SIZE, 32). -export([ old_create_calldata/3 - , create_calldata/5 + , create_calldata/4 , check_calldata/2 , function_type_info/3 , function_type_hash/3 @@ -39,22 +39,11 @@ %%%=================================================================== %%% Handle calldata -create_calldata(Contract, FunName, Args, ArgTypes, RetType) -> - case get_type_info_and_hash(Contract, FunName) of - {ok, TypeInfo, TypeHashInt} -> - Data = aeso_heap:to_binary({TypeHashInt, list_to_tuple(Args)}), - case check_calldata(Data, TypeInfo) of - {ok, CallDataType, OutType} -> - case check_given_type(FunName, ArgTypes, RetType, CallDataType, OutType) of - ok -> - {ok, Data, CallDataType, OutType}; - {error, _} = Err -> - Err - end; - {error,_What} = Err -> Err - end; - {error, _} = Err -> Err - end. +create_calldata(FunName, Args, ArgTypes, RetType) -> + <> = + function_type_hash(list_to_binary(FunName), ArgTypes, RetType), + Data = aeso_heap:to_binary({TypeHashInt, list_to_tuple(Args)}), + {ok, Data, {tuple, [word, {tuple, ArgTypes}]}, RetType}. get_type_info_and_hash(#{type_info := TypeInfo}, FunName) -> FunBin = list_to_binary(FunName), @@ -64,26 +53,6 @@ get_type_info_and_hash(#{type_info := TypeInfo}, FunName) -> {error, _} = Err -> Err end. -%% Check that the given type matches the type from the metadata. -check_given_type(FunName, GivenArgs, GivenRet, CalldataType, ExpectRet) -> - {tuple, [word, {tuple, ExpectArgs}]} = CalldataType, - ReturnOk = if FunName == "init" -> true; - GivenRet == any -> true; - true -> GivenRet == ExpectRet - end, - ArgsOk = ExpectArgs == GivenArgs, - case ReturnOk andalso ArgsOk of - true -> ok; - false when FunName == "init" -> - {error, {init_args_mismatch, - {given, GivenArgs}, - {expected, ExpectArgs}}}; - false -> - {error, {call_type_mismatch, - {given, GivenArgs, '=>', GivenRet}, - {expected, ExpectArgs, '=>', ExpectRet}}} - end. - -spec check_calldata(binary(), type_info()) -> {'ok', typerep(), typerep()} | {'error', atom()}. check_calldata(CallData, TypeInfo) -> diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index dbdd5a0..783613c 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -12,7 +12,7 @@ , file/2 , from_string/2 , check_call/4 - , create_calldata/4 + , create_calldata/3 , version/0 , sophia_type_to_typerep/1 , to_sophia_value/4 @@ -271,13 +271,13 @@ translate_vm_value(VmTypes, {constr_t, _, Con, Types}, Args) translate_vm_value(_VmType, _Type, _Data) -> throw(cannot_translate_to_sophia). --spec create_calldata(map(), string(), string(), [string()]) -> +-spec create_calldata(string(), string(), [string()]) -> {ok, binary(), aeso_sophia:type(), aeso_sophia:type()} | {error, argument_syntax_error}. -create_calldata(Contract, Code, Fun, Args) when is_map(Contract) -> +create_calldata(Code, Fun, Args) -> case check_call(Code, Fun, Args, []) of - {ok, FunName, {ArgTypes, RetType}, Args} -> - aeso_abi:create_calldata(Contract, FunName, Args, ArgTypes, RetType); + {ok, FunName, {ArgTypes, RetType}, VMArgs} -> + aeso_abi:create_calldata(FunName, VMArgs, ArgTypes, RetType); {error, _} = Err -> Err end. From 4c79f7b9f2b73005550812cb200d311308d59be7 Mon Sep 17 00:00:00 2001 From: Ulf Norell Date: Tue, 26 Feb 2019 17:41:04 +0100 Subject: [PATCH 4/6] tests for calldata creation --- test/aeso_abi_tests.erl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index 8557975..f061a83 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -92,6 +92,24 @@ encode_decode_sophia_string(SophiaType, String) -> {error, Err} end. +calldata_test() -> + [42, <<"foobar">>] = encode_decode_calldata(["int", "string"], ["42", "\"foobar\""]), + Map = #{ <<"a">> => 4 }, + [{variant, 1, [Map]}, {{<<"b">>, 5}, {variant, 0, []}}] = + encode_decode_calldata(["variant", "r"], ["Blue({[\"a\"] = 4})", "{x = (\"b\", 5), y = Red}"]), + ok. + +encode_decode_calldata(Types, Args) -> + Code = lists:flatten( + ["contract Dummy =\n", + " type an_alias('a) = (string, 'a)\n" + " record r = {x : an_alias(int), y : variant}\n" + " datatype variant = Red | Blue(map(string, int))\n" + " function foo : (", string:join(Types, ", "), ") => int\n" ]), + {ok, Calldata, CalldataType, word} = aeso_compiler:create_calldata(Code, "foo", Args), + {ok, {_Hash, ArgTuple}} = aeso_heap:from_binary(CalldataType, Calldata), + tuple_to_list(ArgTuple). + encode_decode(T, D) -> ?assertEqual(D, decode(T, encode(D))), D. From 5a3c8530b4661f0f106c919dc2f4497e4dbde1b3 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Tue, 26 Feb 2019 21:03:52 +0100 Subject: [PATCH 5/6] Dialyzer found an error --- src/aeso_abi.erl | 4 ++-- src/aeso_compiler.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aeso_abi.erl b/src/aeso_abi.erl index cea7c45..455ba21 100644 --- a/src/aeso_abi.erl +++ b/src/aeso_abi.erl @@ -40,8 +40,8 @@ %%% Handle calldata create_calldata(FunName, Args, ArgTypes, RetType) -> - <> = - function_type_hash(list_to_binary(FunName), ArgTypes, RetType), + {<>, _, _, _} = + function_type_info(list_to_binary(FunName), ArgTypes, RetType), Data = aeso_heap:to_binary({TypeHashInt, list_to_tuple(Args)}), {ok, Data, {tuple, [word, {tuple, ArgTypes}]}, RetType}. diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 783613c..18354cc 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -273,7 +273,7 @@ translate_vm_value(_VmType, _Type, _Data) -> -spec create_calldata(string(), string(), [string()]) -> {ok, binary(), aeso_sophia:type(), aeso_sophia:type()} - | {error, argument_syntax_error}. + | {error, term()}. create_calldata(Code, Fun, Args) -> case check_call(Code, Fun, Args, []) of {ok, FunName, {ArgTypes, RetType}, VMArgs} -> From a263b09e576e83ba4f4735ed04bde5ffe0b8d27a Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Wed, 27 Feb 2019 11:00:00 +0100 Subject: [PATCH 6/6] Remove leftover io:format --- src/aeso_compiler.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 18354cc..b2fdfd4 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -304,7 +304,6 @@ get_decode_type(FunName, [{contract, _, _, Defs}]) -> GetType = fun({letfun, _, {id, _, Name}, _, Ret, _}) when Name == FunName -> [Ret]; ({fun_decl, _, {id, _, Name}, {fun_t, _, _, _, Ret}}) when Name == FunName -> [Ret]; (_) -> [] end, - io:format("~p\n", [Defs]), case lists:flatmap(GetType, Defs) of [Type] -> {ok, Type}; [] -> {error, missing_function}