diff --git a/.gitignore b/.gitignore index 3b6ef01..45a6ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .rebar3 -_* +_[^_]* .eunit *.o *.beam diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 4ab53a8..4a4537a 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -186,21 +186,21 @@ check_call1(ContractString0, FunName, Args, Options) -> try case proplists:get_value(backend, Options, aevm) of aevm -> - %% First check the contract without the __call function - #{} = string_to_icode(ContractString0, Options), - ContractString = insert_call_function(ContractString0, FunName, Args, Options), - #{typed_ast := TypedAst, - icode := Icode} = string_to_icode(ContractString, Options), - {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), - ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ], - RetVMType = case RetType of - {id, _, "_"} -> any; - _ -> aeso_ast_to_icode:ast_typerep(RetType, Icode) - end, - #{ functions := Funs } = Icode, - ArgIcode = get_arg_icode(Funs), - ArgTerms = [ icode_to_term(T, Arg) || - {T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ], + %% First check the contract without the __call function + #{} = string_to_icode(ContractString0, Options), + ContractString = insert_call_function(ContractString0, ?CALL_NAME, FunName, Args, Options), + #{typed_ast := TypedAst, + icode := Icode} = string_to_icode(ContractString, Options), + {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), + ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ], + RetVMType = case RetType of + {id, _, "_"} -> any; + _ -> aeso_ast_to_icode:ast_typerep(RetType, Icode) + end, + #{ functions := Funs } = Icode, + ArgIcode = get_arg_icode(Funs), + ArgTerms = [ icode_to_term(T, Arg) || + {T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ], RetVMType1 = case FunName of "init" -> {tuple, [typerep, RetVMType]}; @@ -211,13 +211,13 @@ check_call1(ContractString0, FunName, Args, Options) -> %% First check the contract without the __call function #{fcode := OrgFcode} = string_to_fcode(ContractString0, Options), FateCode = aeso_fcode_to_fate:compile(OrgFcode, []), - _SymbolHashes = maps:keys(aeb_fate_code:symbols(FateCode)), - %% TODO collect all hashes and compute the first name without hash collision to - %% be used as __callX - %% case aeb_fate_code:symbol_identifier(<<"__call">>) of - ContractString = insert_call_function(ContractString0, FunName, Args, Options), + %% collect all hashes and compute the first name without hash collision to + SymbolHashes = maps:keys(aeb_fate_code:symbols(FateCode)), + CallName = first_none_match(?CALL_NAME, SymbolHashes, + lists:seq($1, $9) ++ lists:seq($A, $Z) ++ lists:seq($a, $z)), + ContractString = insert_call_function(ContractString0, CallName, FunName, Args, Options), #{fcode := Fcode} = string_to_fcode(ContractString, Options), - #{args := CallArgs} = maps:get({entrypoint, <<"__call">>}, maps:get(functions, Fcode)), + CallArgs = arguments_of_body(CallName, FunName, Fcode), {ok, FunName, CallArgs} end catch @@ -233,17 +233,32 @@ check_call1(ContractString0, FunName, Args, Options) -> fun (E) -> io_lib:format("~p", [E]) end)} end. +arguments_of_body(CallName, _FunName, Fcode) -> + #{body := Body} = maps:get({entrypoint, list_to_binary(CallName)}, maps:get(functions, Fcode)), + {def, _FName, Args} = Body, + %% FName is either {entrypoint, list_to_binary(FunName)} or 'init' + [ aeso_fcode_to_fate:term_to_fate(A) || A <- Args ]. + +first_none_match(_CallName, _Hashes, []) -> + error(unable_to_find_unique_call_name); +first_none_match(CallName, Hashes, [Char|Chars]) -> + case not lists:member(aeb_fate_code:symbol_identifier(list_to_binary(CallName)), Hashes) of + true -> + CallName; + false -> + first_none_match(?CALL_NAME++[Char], Hashes, Chars) + end. %% Add the __call function to a contract. --spec insert_call_function(string(), string(), [string()], options()) -> string(). -insert_call_function(Code, FunName, Args, Options) -> +-spec insert_call_function(string(), string(), string(), [string()], options()) -> string(). +insert_call_function(Code, Call, FunName, Args, Options) -> Ast = parse(Code, Options), Ind = last_contract_indent(Ast), lists:flatten( [ Code, "\n\n", lists:duplicate(Ind, " "), - "stateful function __call() = ", FunName, "(", string:join(Args, ","), ")\n" + "stateful function ", Call,"() = ", FunName, "(", string:join(Args, ","), ")\n" ]). -spec insert_init_function(string(), options()) -> string(). diff --git a/src/aeso_fcode_to_fate.erl b/src/aeso_fcode_to_fate.erl index 1efc581..b6d6817 100644 --- a/src/aeso_fcode_to_fate.erl +++ b/src/aeso_fcode_to_fate.erl @@ -9,7 +9,7 @@ %%%------------------------------------------------------------------- -module(aeso_fcode_to_fate). --export([compile/2]). +-export([compile/2, term_to_fate/1]). %% -- Preamble --------------------------------------------------------------- @@ -231,16 +231,36 @@ lookup_var(#env{vars = Vars}, X) -> %% -- The compiler -- -to_scode(_Env, {lit, L}) -> - case L of - {int, N} -> [push(?i(N))]; - {string, S} -> [push(?i(aeb_fate_data:make_string(S)))]; - {bool, B} -> [push(?i(B))]; - {account_pubkey, K} -> [push(?i(aeb_fate_data:make_address(K)))]; - {contract_pubkey, K} -> [push(?i(aeb_fate_data:make_contract(K)))]; - {oracle_pubkey, K} -> [push(?i(aeb_fate_data:make_oracle(K)))]; +lit_to_fate(L) -> + case L of + {int, N} -> aeb_fate_data:make_integer(N); + {string, S} -> aeb_fate_data:make_string(S); + {bool, B} -> aeb_fate_data:make_boolean(B); + {account_pubkey, K} -> aeb_fate_data:make_address(K); + {contract_pubkey, K} -> aeb_fate_data:make_contract(K); + {oracle_pubkey, K} -> aeb_fate_data:make_oracle(K); {oracle_query_id, _} -> ?TODO(fate_oracle_query_id_value) - end; + end. + +term_to_fate({lit, L}) -> + lit_to_fate(L); +%% negative literals are parsed as 0 - N +term_to_fate({op, '-', [{lit, {int, 0}}, {lit, {int, N}}]}) -> + aeb_fate_data:make_integer(-N); +term_to_fate(nil) -> + aeb_fate_data:make_list([]); +term_to_fate({op, '::', [Hd, Tl]}) -> + %% The Tl will translate into a list, because FATE lists are just lists + [term_to_fate(Hd) | term_to_fate(Tl)]; +term_to_fate({tuple, As}) -> + aeb_fate_data:make_tuple(list_to_tuple([ term_to_fate(A) || A<-As])); +term_to_fate({con, Ar, I, As}) -> + FateAs = [ term_to_fate(A) || A <- As ], + aeb_fate_data:make_variant(Ar, I, list_to_tuple(FateAs)). + + +to_scode(_Env, {lit, L}) -> + [push(?i(lit_to_fate(L)))]; to_scode(_Env, nil) -> [aeb_fate_ops:nil(?a)]; diff --git a/test/aeso_calldata_tests.erl b/test/aeso_calldata_tests.erl new file mode 100644 index 0000000..3b19d3f --- /dev/null +++ b/test/aeso_calldata_tests.erl @@ -0,0 +1,78 @@ +%%% -*- erlang-indent-level:4; indent-tabs-mode: nil -*- +%%%------------------------------------------------------------------- +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc Test Sophia language compiler. +%%% +%%% @end +%%%------------------------------------------------------------------- + +-module(aeso_calldata_tests). + +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). + +%% Very simply test compile the given contracts. Only basic checks +%% are made on the output, just that it is a binary which indicates +%% that the compilation worked. +calldata_test_() -> + [ {"Testing the " ++ ContractName ++ " contract with the " ++ atom_to_list(Backend) ++ " backend", + fun() -> + ContractString = aeso_test_utils:read_contract(ContractName), + Res = aeso_compiler:create_calldata(ContractString, Fun, Args, [{backend, Backend}]), + case Backend of + aevm -> + ?assertMatch({ok, _, _, _}, Res); + fate -> + ?assertMatch({ok, _}, Res) + end + end} || {ContractName, Fun, Args} <- compilable_contracts(), Backend <- [aevm, fate], + not lists:member(ContractName, not_yet_compilable(Backend))]. + +check_errors(Expect, ErrorString) -> + %% This removes the final single \n as well. + Actual = binary:split(<>, <<"\n\n">>, [global,trim]), + case {Expect -- Actual, Actual -- Expect} of + {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); + {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); + {Missing, Extra} -> ?assertEqual(Missing, Extra) + end. + +%% compilable_contracts() -> [ContractName]. +%% The currently compilable contracts. + +compilable_contracts() -> + [ + {"identity", "init", []}, + {"maps", "init", []}, + {"oracles", "init", []}, + {"variant_types", "init", []}, + {"basic_auth", "init", []}, + {"address_literals", "init", []}, + {"bytes_equality", "init", []}, + {"address_chain", "init", []}, + {"counter", "init", + ["-3334353637383940202122232425262728293031323334353637"]}, + {"dutch_auction", "init", + ["ak_2gx9MEFxKvY9vMG5YnqnXWv1hCsX7rgnfvBLJS4aQurustR1rt", "200000", "1000"]}, + {"maps", "fromlist_i", + ["[(1, {x = 1, y = 2}), (2, {x = 3, y = 4}), (3, {x = 4, y = 4})]"]}, + {"strings", "str_concat", ["\"test\"","\"me\""]}, + {"complex_types", "filter_some", ["[Some(1), Some(2), None]"]}, + {"complex_types", "init", ["ct_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ"]}, + {"__call" "init", []}, + {"bitcoin_auth", "init", + ["#0102030405060708090a0b0c0d0e0f1017181920212223242526272829303132" + "33343536373839401a1b1c1d1e1f202122232425262728293031323334353637"]} + ]. + +not_yet_compilable(fate) -> + ["oracles", %% Oracle.register + "events", + "basic_auth", %% auth_tx_hash instruction + "bitcoin_auth", %% fate_auth_tx_hash_instruction + "address_literals", %% oracle_query_id literals + "address_chain" %% Oracle.check_query + ]; +not_yet_compilable(aevm) -> + ["__call"]. diff --git a/test/contracts/__call.aes b/test/contracts/__call.aes new file mode 100644 index 0000000..1e19b5b --- /dev/null +++ b/test/contracts/__call.aes @@ -0,0 +1,5 @@ + +contract Identity = + function main (x:int) = x + + function __call() = 12