From 510935d945d577d27e9f5e2fdf38409a7541edf4 Mon Sep 17 00:00:00 2001 From: Ulf Norell Date: Tue, 3 Sep 2019 10:21:37 +0200 Subject: [PATCH] Framework and tests for code generation (icode/fcode) errors --- src/aeso_aci.erl | 12 +- src/aeso_ast_infer_types.erl | 2 +- src/aeso_ast_to_fcode.erl | 56 ++- src/aeso_ast_to_icode.erl | 39 +- src/aeso_code_errors.erl | 75 ++++ src/aeso_compiler.erl | 39 +- src/aeso_errors.erl | 9 +- src/aeso_parser.erl | 4 +- test/aeso_abi_tests.erl | 2 +- test/aeso_compiler_tests.erl | 402 ++++++++++++------ test/aeso_parser_tests.erl | 2 +- .../code_errors/bad_aens_resolve.aes | 9 + .../code_errors/higher_order_compare.aes | 8 + .../last_declaration_must_be_contract.aes | 2 + .../code_errors/missing_init_function.aes | 3 + .../code_errors/parameterised_event.aes | 4 + .../code_errors/parameterised_state.aes | 4 + .../code_errors/polymorphic_aens_resolve.aes | 7 + .../code_errors/polymorphic_compare.aes | 7 + .../code_errors/polymorphic_entrypoint.aes | 3 + 20 files changed, 481 insertions(+), 208 deletions(-) create mode 100644 src/aeso_code_errors.erl create mode 100644 test/contracts/code_errors/bad_aens_resolve.aes create mode 100644 test/contracts/code_errors/higher_order_compare.aes create mode 100644 test/contracts/code_errors/last_declaration_must_be_contract.aes create mode 100644 test/contracts/code_errors/missing_init_function.aes create mode 100644 test/contracts/code_errors/parameterised_event.aes create mode 100644 test/contracts/code_errors/parameterised_state.aes create mode 100644 test/contracts/code_errors/polymorphic_aens_resolve.aes create mode 100644 test/contracts/code_errors/polymorphic_compare.aes create mode 100644 test/contracts/code_errors/polymorphic_entrypoint.aes diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 8bd329b..7f9f7a9 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -74,19 +74,9 @@ do_contract_interface(Type, ContractString, Options) -> string -> do_render_aci_json(JArray) end catch - throw:{type_errors, Errors} -> {error, Errors}; - %% The compiler errors. - error:{parse_errors, Errors} -> - {error, join_errors("Parse errors", Errors, fun(E) -> E end)}; - error:{code_errors, Errors} -> - {error, join_errors("Code errors", Errors, - fun (E) -> io_lib:format("~p", [E]) end)} + throw:{error, Errors} -> {error, Errors} end. -join_errors(Prefix, Errors, Pfun) -> - Ess = [ Pfun(E) || E <- Errors ], - list_to_binary(string:join([Prefix|Ess], "\n")). - encode_contract(Contract = {contract, _, {con, _, Name}, _}) -> C0 = #{name => encode_name(Name)}, diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 11ecae1..88fdc12 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -2069,7 +2069,7 @@ destroy_and_report_type_errors(Env) -> %% io:format("Type errors now: ~p\n", [Errors0]), ets_delete(type_errors), Errors = [ mk_error(unqualify(Env, Err)) || Err <- Errors0 ], - Errors == [] orelse throw({type_errors, Errors}). + aeso_errors:throw(Errors). %% No-op if Errors == [] %% Strip current namespace from error message for nicer printing. unqualify(#env{ namespace = NS }, {qid, Ann, Xs}) -> diff --git a/src/aeso_ast_to_fcode.erl b/src/aeso_ast_to_fcode.erl index 82c276b..f6ca217 100644 --- a/src/aeso_ast_to_fcode.erl +++ b/src/aeso_ast_to_fcode.erl @@ -232,7 +232,7 @@ is_no_code(Env) -> %% -- Compilation ------------------------------------------------------------ -spec to_fcode(env(), aeso_syntax:ast()) -> fcode(). -to_fcode(Env, [{contract, Attrs, {con, _, Main}, Decls}]) -> +to_fcode(Env, [{contract, Attrs, MainCon = {con, _, Main}, Decls}]) -> #{ builtins := Builtins } = Env, MainEnv = Env#{ context => {main_contract, Main}, builtins => Builtins#{[Main, "state"] => {get_state, none}, @@ -247,8 +247,10 @@ to_fcode(Env, [{contract, Attrs, {con, _, Main}, Decls}]) -> state_type => StateType, event_type => EventType, payable => Payable, - functions => add_init_function(Env1, StateType, + functions => add_init_function(Env1, MainCon, StateType, add_event_function(Env1, EventType, Funs)) }; +to_fcode(_Env, [NotContract]) -> + fcode_error({last_declaration_must_be_contract, NotContract}); to_fcode(Env, [{contract, _, {con, _, Con}, Decls} | Code]) -> Env1 = decls_to_fcode(Env#{ context => {abstract_contract, Con} }, Decls), to_fcode(Env1, Code); @@ -294,9 +296,10 @@ decl_to_fcode(Env = #{ functions := Funs }, {letfun, Ann, {id, _, Name}, Args, R Env#{ functions := NewFuns }. -spec typedef_to_fcode(env(), aeso_syntax:id(), [aeso_syntax:tvar()], aeso_syntax:typedef()) -> env(). -typedef_to_fcode(Env, {id, _, Name}, Xs, Def) -> +typedef_to_fcode(Env, Id = {id, _, Name}, Xs, Def) -> + check_state_and_event_types(Env, Id, Xs), Q = qname(Env, Name), - FDef = fun(Args) -> + FDef = fun(Args) when length(Args) == length(Xs) -> Sub = maps:from_list(lists:zip([X || {tvar, _, X} <- Xs], Args)), case Def of {record_t, Fields} -> {todo, Xs, Args, record_t, Fields}; @@ -307,7 +310,9 @@ typedef_to_fcode(Env, {id, _, Name}, Xs, Def) -> end || Con <- Cons ], {variant, FCons}; {alias_t, Type} -> {todo, Xs, Args, alias_t, Type} - end end, + end; + (Args) -> internal_error({type_arity_mismatch, Name, length(Args), length(Xs)}) + end, Constructors = case Def of {variant_t, Cons} -> @@ -328,6 +333,14 @@ typedef_to_fcode(Env, {id, _, Name}, Xs, Def) -> end, bind_type(Env2, Q, FDef). +check_state_and_event_types(#{ context := {main_contract, _} }, Id, [_ | _]) -> + case Id of + {id, _, "state"} -> fcode_error({parameterized_state, Id}); + {id, _, "event"} -> fcode_error({parameterized_event, Id}); + _ -> ok + end; +check_state_and_event_types(_, _, _) -> ok. + -spec type_to_fcode(env(), aeso_syntax:type()) -> ftype(). type_to_fcode(Env, Type) -> type_to_fcode(Env, #{}, Type). @@ -533,7 +546,7 @@ expr_to_fcode(Env, Type, {app, _, Fun = {typed, _, _, {fun_t, _, NamedArgsT, _, {builtin_u, B, _} when B =:= aens_resolve -> %% Get the type we are assuming the name resolves to AensType = type_to_fcode(Env, Type), - validate_aens_resolve_type(aeso_syntax:get_ann(Fun), AensType), + validate_aens_resolve_type(aeso_syntax:get_ann(Fun), Type, AensType), TypeArgs = [{lit, {typerep, AensType}}], builtin_to_fcode(B, FArgs ++ TypeArgs); {builtin_u, B, _Ar} -> builtin_to_fcode(B, FArgs); @@ -616,10 +629,15 @@ validate_oracle_type(Ann, QType, RType) -> ensure_first_order(RType, {higher_order_response_type, Ann, RType}), ok. -validate_aens_resolve_type(Ann, {variant, [[], [Type]]}) -> - ensure_monomorphic(Type, {polymorphic_aens_resolve, Ann, Type}), - ensure_first_order(Type, {higher_order_aens_resolve, Ann, Type}), - ok. +validate_aens_resolve_type(Ann, {app_t, _, _, [Type]}, {variant, [[], [FType]]}) -> + case FType of + string -> ok; + address -> ok; + contract -> ok; + {oracle, _, _} -> ok; + oracle_query -> ok; + _ -> fcode_error({invalid_aens_resolve_type, Ann, Type}) + end. ensure_first_order_entrypoint(Ann, Args, Ret) -> [ ensure_first_order(T, {higher_order_entrypoint_argument, Ann, X, T}) @@ -904,18 +922,18 @@ builtin_to_fcode(Builtin, Args) -> %% -- Init function -- -add_init_function(Env, StateType, Funs0) -> +add_init_function(Env, Main, StateType, Funs0) -> case is_no_code(Env) of true -> Funs0; false -> - Funs = add_default_init_function(Env, StateType, Funs0), + Funs = add_default_init_function(Env, Main, StateType, Funs0), InitName = {entrypoint, <<"init">>}, InitFun = #{ body := InitBody} = maps:get(InitName, Funs), Funs#{ InitName => InitFun#{ return => {tuple, []}, body => {builtin, set_state, [InitBody]} } } end. -add_default_init_function(_Env, StateType, Funs) -> +add_default_init_function(_Env, Main, StateType, Funs) -> InitName = {entrypoint, <<"init">>}, case maps:get(InitName, Funs, none) of %% Only add default init function if state is unit. @@ -924,7 +942,7 @@ add_default_init_function(_Env, StateType, Funs) -> args => [], return => {tuple, []}, body => {tuple, []}} }; - none -> fcode_error(missing_init_function); + none -> fcode_error({missing_init_function, Main}); _ -> Funs end. @@ -1115,7 +1133,7 @@ lookup_type(Env, {qid, _, Name}, Args) -> lookup_type(Env, Name, Args); lookup_type(Env, Name, Args) -> case lookup_type(Env, Name, Args, not_found) of - not_found -> error({unknown_type, Name}); + not_found -> internal_error({unknown_type, Name}); Type -> Type end. @@ -1440,8 +1458,12 @@ get_attributes(Ann) -> indexed(Xs) -> lists:zip(lists:seq(1, length(Xs)), Xs). -fcode_error(Err) -> - error(Err). +fcode_error(Error) -> + aeso_errors:throw(aeso_code_errors:format(Error)). + +internal_error(Error) -> + Msg = lists:flatten(io_lib:format("~p\n", [Error])), + aeso_errors:throw(aeso_errors:new(internal_error, aeso_errors:pos(0, 0), Msg)). %% -- Pretty printing -------------------------------------------------------- diff --git a/src/aeso_ast_to_icode.erl b/src/aeso_ast_to_icode.erl index 728d60b..9918a3b 100644 --- a/src/aeso_ast_to_icode.erl +++ b/src/aeso_ast_to_icode.erl @@ -21,8 +21,8 @@ convert_typed(TypedTree, Options) -> case lists:last(TypedTree) of {contract, Attrs, {con, _, Con}, _} -> {proplists:get_value(payable, Attrs, false), Con}; - _ -> - gen_error(last_declaration_must_be_contract) + Decl -> + gen_error({last_declaration_must_be_contract, Decl}) end, NewIcode = aeso_icode:set_payable(Payable, aeso_icode:set_name(Name, aeso_icode:new(Options))), @@ -42,17 +42,17 @@ code([], Icode, Options) -> %% Generate error on correct format. gen_error(Error) -> - error({code_errors, [Error]}). + aeso_errors:throw(aeso_code_errors:format(Error)). %% Create default init function (only if state is unit). -add_default_init_function(Icode = #{functions := Funs, state_type := State}, Options) -> +add_default_init_function(Icode = #{namespace := NS, functions := Funs, state_type := State}, Options) -> NoCode = proplists:get_value(no_code, Options, false), {_, _, QInit} = aeso_icode:qualify({id, [], "init"}, Icode), case lists:keymember(QInit, 1, Funs) of true -> Icode; false when NoCode -> Icode; false when State /= {tuple, []} -> - gen_error(missing_init_function); + gen_error({missing_init_function, NS}); false -> Type = {tuple, [typerep, {tuple, []}]}, Value = #tuple{ cpts = [type_value({tuple, []}), {tuple, []}] }, @@ -83,9 +83,9 @@ contract_to_icode([{type_def, _Attrib, Id = {id, _, Name}, Args, Def} | Rest], constructors := maps:merge(Constructors, NewConstructors) }, Icode2 = case Name of "state" when Args == [] -> Icode1#{ state_type => ast_typerep(Def, Icode) }; - "state" -> gen_error(state_type_cannot_be_parameterized); + "state" -> gen_error({parameterized_state, Id}); "event" when Args == [] -> Icode1#{ event_type => Def }; - "event" -> gen_error(event_type_cannot_be_parameterized); + "event" -> gen_error({parameterized_event, Id}); _ -> Icode1 end, contract_to_icode(Rest, Icode2); @@ -398,10 +398,8 @@ ast_binop(Op, Ann, {typed, _, A, Type}, B, Icode) when Op == '=='; Op == '!='; Op == '<'; Op == '>'; Op == '<='; Op == '=<'; Op == '>=' -> - Monomorphic = is_monomorphic(Type), + [ gen_error({cant_compare_type_aevm, Ann, Op, Type}) || not is_simple_type(Type) ], case ast_typerep(Type, Icode) of - _ when not Monomorphic -> - gen_error({cant_compare_polymorphic_type, Ann, Op, Type}); word -> #binop{op = Op, left = ast_body(A, Icode), right = ast_body(B, Icode)}; OtherType -> Neg = case Op of @@ -767,14 +765,22 @@ map_upd(Key, Default, ValFun, Map = {typed, Ann, _, MapType}, Icode) -> builtin_call(FunName, Args). check_entrypoint_type(Ann, Name, Args, Ret) -> - Check = fun(T, Err) -> - case is_simple_type(T) of + CheckFirstOrder = fun(T, Err) -> + case is_first_order_type(T) of false -> gen_error(Err); true -> ok end end, - [ Check(T, {entrypoint_argument_must_have_simple_type, Ann1, Name, X, T}) + CheckMonomorphic = fun(T, Err) -> + case is_monomorphic(T) of + false -> gen_error(Err); + true -> ok + end end, + [ CheckFirstOrder(T, {entrypoint_argument_must_have_first_order_type, Ann1, Name, X, T}) || {arg, Ann1, X, T} <- Args ], - Check(Ret, {entrypoint_must_have_simple_return_type, Ann, Name, Ret}). + CheckFirstOrder(Ret, {entrypoint_must_have_first_order_return_type, Ann, Name, Ret}), + [ CheckMonomorphic(T, {entrypoint_argument_must_have_monomorphic_type, Ann1, Name, X, T}) + || {arg, Ann1, X, T} <- Args ], + CheckMonomorphic(Ret, {entrypoint_must_have_monomorphic_return_type, Ann, Name, Ret}). is_simple_type({tvar, _, _}) -> false; is_simple_type({fun_t, _, _, _, _}) -> false; @@ -782,6 +788,11 @@ is_simple_type(Ts) when is_list(Ts) -> lists:all(fun is_simple_type/1, Ts); is_simple_type(T) when is_tuple(T) -> is_simple_type(tuple_to_list(T)); is_simple_type(_) -> true. +is_first_order_type({fun_t, _, _, _, _}) -> false; +is_first_order_type(Ts) when is_list(Ts) -> lists:all(fun is_first_order_type/1, Ts); +is_first_order_type(T) when is_tuple(T) -> is_first_order_type(tuple_to_list(T)); +is_first_order_type(_) -> true. + is_monomorphic({tvar, _, _}) -> false; is_monomorphic([H|T]) -> is_monomorphic(H) andalso is_monomorphic(T); diff --git a/src/aeso_code_errors.erl b/src/aeso_code_errors.erl new file mode 100644 index 0000000..22b31c6 --- /dev/null +++ b/src/aeso_code_errors.erl @@ -0,0 +1,75 @@ +%%%------------------------------------------------------------------- +%%% @author Ulf Norell +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc +%%% Formatting of code generation errors. +%%% @end +%%% +%%%------------------------------------------------------------------- +-module(aeso_code_errors). + +-export([format/1]). + +format({last_declaration_must_be_contract, Decl = {namespace, _, {con, _, C}, _}}) -> + Msg = io_lib:format("Expected a contract as the last declaration instead of the namespace '~s'\n", + [C]), + mk_err(pos(Decl), Msg); +format({missing_init_function, Con}) -> + Msg = io_lib:format("Missing init function for the contract '~s'.\n", [pp_expr(Con)]), + Cxt = "The 'init' function can only be omitted if the state type is 'unit'.\n", + mk_err(pos(Con), Msg, Cxt); +format({parameterized_state, Decl}) -> + Msg = "The state type cannot be parameterized.\n", + mk_err(pos(Decl), Msg); +format({parameterized_event, Decl}) -> + Msg = "The event type cannot be parameterized.\n", + mk_err(pos(Decl), Msg); +format({entrypoint_argument_must_have_monomorphic_type, Ann, {id, _, Name}, X, T}) -> + Msg = io_lib:format("The argument\n~s\nof entrypoint '~s' does not have a monomorphic type.\n", + [pp_typed(X, T), Name]), + Cxt = "Use the FATE backend if you want polymorphic entrypoints.", + mk_err(pos(Ann), Msg, Cxt); +format({cant_compare_type_aevm, Ann, Op, Type}) -> + StringAndTuple = [ "- type string\n" + "- tuple or record of word type\n" || lists:member(Op, ['==', '!=']) ], + Msg = io_lib:format("Cannot compare values of type\n" + "~s\n" + "The AEVM only supports '~s' on values of\n" + "- word type (int, bool, bits, address, oracle(_, _), etc)\n" + "~s" + "Use FATE if you need to compare arbitrary types.\n", + [pp_type(2, Type), Op, StringAndTuple]), + mk_err(pos(Ann), Msg); +format({invalid_aens_resolve_type, Ann, T}) -> + Msg = io_lib:format("Invalid return type of AENS.resolve:\n" + "~s\n" + "It must be a string or a pubkey type (address, oracle, etc).\n", + [pp_type(2, T)]), + mk_err(pos(Ann), Msg); +format(Err) -> + mk_err(aeso_errors:pos(0, 0), io_lib:format("Unknown error: ~p\n", [Err])). + +pos(Ann) -> + File = aeso_syntax:get_ann(file, Ann, no_file), + Line = aeso_syntax:get_ann(line, Ann, 0), + Col = aeso_syntax:get_ann(col, Ann, 0), + aeso_errors:pos(File, Line, Col). + +pp_typed(E, T) -> + prettypr:format(prettypr:nest(2, + lists:foldr(fun prettypr:beside/2, prettypr:empty(), + [aeso_pretty:expr(E), prettypr:text(" : "), + aeso_pretty:type(T)]))). + +pp_expr(E) -> + prettypr:format(aeso_pretty:expr(E)). + +pp_type(N, T) -> + prettypr:format(prettypr:nest(N, aeso_pretty:type(T))). + +mk_err(Pos, Msg) -> + aeso_errors:new(code_error, Pos, lists:flatten(Msg)). + +mk_err(Pos, Msg, Cxt) -> + aeso_errors:new(code_error, Pos, lists:flatten(Msg), lists:flatten(Cxt)). + diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 0939c75..3ee0d48 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -98,15 +98,7 @@ from_string(Backend, ContractString, Options) -> try from_string1(Backend, ContractString, Options) catch - %% The compiler errors. - throw:{parse_errors, Errors} -> - {error, Errors}; - throw:{type_errors, Errors} -> - {error, Errors}; - error:{code_errors, Errors} -> - {error, join_errors("Code errors", Errors, - fun (E) -> io_lib:format("~p", [E]) end)} - %% General programming errors in the compiler just signal error. + throw:{error, Errors} -> {error, Errors} end. from_string1(aevm, ContractString, Options) -> @@ -230,16 +222,10 @@ check_call1(ContractString0, FunName, Args, Options) -> {ok, FunName, CallArgs} end catch - throw:{parse_errors, Errors} -> - {error, Errors}; - throw:{type_errors, Errors} -> - {error, Errors}; + throw:{error, Errors} -> {error, Errors}; error:{badmatch, {error, missing_call_function}} -> {error, join_errors("Type errors", ["missing __call function"], - fun (E) -> E end)}; - throw:Error -> %Don't ask - {error, join_errors("Code errors", [Error], - fun (E) -> io_lib:format("~p", [E]) end)} + fun (E) -> E end)} end. arguments_of_body(CallName, _FunName, Fcode) -> @@ -345,16 +331,10 @@ to_sophia_value(ContractString, FunName, ok, Data, Options0) -> end end catch - throw:{parse_errors, Errors} -> - {error, Errors}; - throw:{type_errors, Errors} -> - {error, Errors}; + throw:{error, Errors} -> {error, Errors}; 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], - fun (E) -> io_lib:format("~p", [E]) end)} + fun (E) -> E end)} end. @@ -444,16 +424,11 @@ decode_calldata(ContractString, FunName, Calldata, Options0) -> end end catch - throw:{parse_errors, Errors} -> - {error, Errors}; - throw:{type_errors, Errors} -> + throw:{error, Errors} -> {error, Errors}; 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], - fun (E) -> io_lib:format("~p", [E]) end)} + fun (E) -> E end)} end. get_arg_icode(Funs) -> diff --git a/src/aeso_errors.erl b/src/aeso_errors.erl index a9e6419..b96794c 100644 --- a/src/aeso_errors.erl +++ b/src/aeso_errors.erl @@ -34,6 +34,7 @@ , pos/2 , pos/3 , pp/1 + , throw/1 , type/1 ]). @@ -49,6 +50,12 @@ pos(Line, Col) -> pos(File, Line, Col) -> #pos{ file = File, line = Line, col = Col }. +throw([]) -> ok; +throw(Errs) when is_list(Errs) -> + erlang:throw({error, Errs}); +throw(#err{} = Err) -> + erlang:throw({error, [Err]}). + msg(#err{ message = Msg, context = none }) -> Msg; msg(#err{ message = Msg, context = Ctxt }) -> Msg ++ Ctxt. @@ -68,4 +75,4 @@ pp(#err{ pos = Pos } = Err) -> pp_pos(#pos{file = no_file, line = L, col = C}) -> io_lib:format("At line ~p, col ~p:", [L, C]); pp_pos(#pos{file = F, line = L, col = C}) -> - io_lib:format("In '~s' at line ~p, col~p:", [F, L, C]). + io_lib:format("In '~s' at line ~p, col ~p:", [F, L, C]). diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 97c5ae2..6eecca7 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -33,10 +33,10 @@ string(String, Included, Opts) -> {ok, AST} -> case expand_includes(AST, Included, Opts) of {ok, AST1} -> AST1; - {error, Err} -> throw({parse_errors, [mk_error(Err)]}) + {error, Err} -> aeso_errors:throw(mk_error(Err)) end; {error, Err} -> - throw({parse_errors, [mk_error(Err)]}) + aeso_errors:throw(mk_error(Err)) end. type(String) -> diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index 481c8c7..8c585e0 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -163,7 +163,7 @@ permissive_literals_fail_test() -> " Chain.spend(o, 1000000)\n", {error, [Err]} = aeso_compiler:check_call(Contract, "haxx", ["#123"], []), - ?assertMatch("Cannot unify" ++ _, aeso_errors:pp(Err)), + ?assertMatch("At line 3, col 5:\nCannot unify" ++ _, aeso_errors:pp(Err)), ?assertEqual(type_error, aeso_errors:type(Err)), ok. diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 39efe4c..68efabf 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -16,57 +16,74 @@ %% are made on the output, just that it is a binary which indicates %% that the compilation worked. simple_compile_test_() -> - [ {"Testing the " ++ ContractName ++ " contract with the " ++ atom_to_list(Backend) ++ " backend", - fun() -> - case compile(Backend, ContractName) of - #{byte_code := ByteCode, - contract_source := _, - type_info := _} when Backend == aevm -> - ?assertMatch(Code when is_binary(Code), ByteCode); - #{fate_code := Code} when Backend == fate -> - Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), - ?assertMatch({X, X}, {Code1, Code}); - ErrBin -> - io:format("\n~s", [ErrBin]), - error(ErrBin) - end - end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], - not lists:member(ContractName, not_yet_compilable(Backend))] ++ - [ {"Testing error messages of " ++ ContractName, - fun() -> - case compile(aevm, ContractName) of - <<"Parse errors\n", ErrorString/binary>> -> - check_errors(lists:sort(ExpectedErrors), ErrorString); - Errors -> - check_errors(lists:sort(ExpectedErrors), Errors) - end - end} || - {ContractName, ExpectedErrors} <- failing_contracts() ] ++ - [ {"Testing include with explicit files", - fun() -> - FileSystem = maps:from_list( - [ begin - {ok, Bin} = file:read_file(filename:join([aeso_test_utils:contract_path(), File])), - {File, Bin} - end || File <- ["included.aes", "../contracts/included2.aes"] ]), - #{byte_code := Code1} = compile(aevm, "include", [{include, {explicit_files, FileSystem}}]), - #{byte_code := Code2} = compile(aevm, "include"), - ?assertMatch(true, Code1 == Code2) - end} ] ++ - [ {"Testing deadcode elimination for " ++ atom_to_list(Backend), - fun() -> - #{ byte_code := NoDeadCode } = compile(Backend, "nodeadcode"), - #{ byte_code := DeadCode } = compile(Backend, "deadcode"), - SizeNoDeadCode = byte_size(NoDeadCode), - SizeDeadCode = byte_size(DeadCode), - Delta = if Backend == aevm -> 40; - Backend == fate -> 20 end, - ?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}), - ok - end} || Backend <- [aevm, fate] ]. + [ {"Testing the " ++ ContractName ++ " contract with the " ++ atom_to_list(Backend) ++ " backend", + fun() -> + case compile(Backend, ContractName) of + #{byte_code := ByteCode, + contract_source := _, + type_info := _} when Backend == aevm -> + ?assertMatch(Code when is_binary(Code), ByteCode); + #{fate_code := Code} when Backend == fate -> + Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), + ?assertMatch({X, X}, {Code1, Code}); + ErrBin -> + io:format("\n~s", [ErrBin]), + error(ErrBin) + end + end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], + not lists:member(ContractName, not_yet_compilable(Backend))] ++ + [ {"Testing error messages of " ++ ContractName, + fun() -> + Errors = compile(aevm, ContractName), + check_errors(ExpectedErrors, Errors) + end} || + {ContractName, ExpectedErrors} <- failing_contracts() ] ++ + [ {"Testing " ++ atom_to_list(Backend) ++ " code generation error messages of " ++ ContractName, + fun() -> + Errors = compile(Backend, ContractName), + Expect = + case is_binary(ExpectedError) of + true -> [ExpectedError]; + false -> + case proplists:get_value(Backend, ExpectedError, no_error) of + no_error -> no_error; + Err -> [Err] + end + end, + check_errors(Expect, Errors) + end} || + {ContractName, ExpectedError} <- failing_code_gen_contracts(), + Backend <- [aevm, fate] ] ++ + [ {"Testing include with explicit files", + fun() -> + FileSystem = maps:from_list( + [ begin + {ok, Bin} = file:read_file(filename:join([aeso_test_utils:contract_path(), File])), + {File, Bin} + end || File <- ["included.aes", "../contracts/included2.aes"] ]), + #{byte_code := Code1} = compile(aevm, "include", [{include, {explicit_files, FileSystem}}]), + #{byte_code := Code2} = compile(aevm, "include"), + ?assertMatch(true, Code1 == Code2) + end} ] ++ + [ {"Testing deadcode elimination for " ++ atom_to_list(Backend), + fun() -> + #{ byte_code := NoDeadCode } = compile(Backend, "nodeadcode"), + #{ byte_code := DeadCode } = compile(Backend, "deadcode"), + SizeNoDeadCode = byte_size(NoDeadCode), + SizeDeadCode = byte_size(DeadCode), + Delta = if Backend == aevm -> 40; + Backend == fate -> 20 end, + ?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}), + ok + end} || Backend <- [aevm, fate] ] ++ + []. -check_errors(Expect, Actual0) -> - Actual = [ list_to_binary(string:trim(aeso_errors:msg(Err))) || Err <- Actual0 ], +check_errors(no_error, Actual) -> ?assertMatch(#{}, Actual); +check_errors(Expect, #{}) -> + ?assertEqual({error, Expect}, ok); +check_errors(Expect0, Actual0) -> + Expect = lists:sort(Expect0), + Actual = [ list_to_binary(string:trim(aeso_errors:pp(Err))) || Err <- Actual0 ], case {Expect -- Actual, Actual -- Expect} of {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); @@ -79,7 +96,7 @@ compile(Backend, Name) -> compile(Backend, Name, Options) -> String = aeso_test_utils:read_contract(Name), - case aeso_compiler:from_string(String, [{src_file, Name}, {backend, Backend} | Options]) of + case aeso_compiler:from_string(String, [{src_file, Name ++ ".aes"}, {backend, Backend} | Options]) of {ok, Map} -> Map; {error, ErrorString} when is_binary(ErrorString) -> ErrorString; {error, Errors} -> Errors @@ -135,241 +152,370 @@ not_yet_compilable(aevm) -> []. %% Contracts that should produce type errors +-define(Pos(Line, Col), "At line " ??Line ", col " ??Col ":\n"). +-define(Pos(File, Line, Col), "In '" File ".aes' at line " ??Line ", col " ??Col ":\n"). + failing_contracts() -> [ {"name_clash", - [<<"Duplicate definitions of abort at\n" + [<>, - <<"Duplicate definitions of require at\n" + <>, - <<"Duplicate definitions of double_def at\n" + <>, - <<"Duplicate definitions of double_proto at\n" + <>, - <<"Duplicate definitions of proto_and_def at\n" + <>, - <<"Duplicate definitions of put at\n" + <>, - <<"Duplicate definitions of state at\n" + <>]} , {"type_errors", - [<<"Unbound variable zz at line 17, column 23">>, - <<"Cannot unify int\n" + [<>, + < list(int)\n" "to arguments\n" " x : int\n" " x : int">>, - <<"Cannot unify string\n" + <>, - <<"Cannot unify int\n" + <>, - <<"Cannot unify string\n" + <>, - <<"Cannot unify string\n" + <>, - <<"Cannot unify string\n" + <>, - <<"Cannot unify int\n" + <>, - <<"Not a record type: string\n" + <>, - <<"Not a record type: string\n" + <>, - <<"Not a record type: string\n" + <>, - <<"Not a record type: string\n" + <>, - <<"Ambiguous record type with field y (at line 13, column 27) could be one of\n" + <>, - <<"Repeated name x in pattern\n" + <>, - <<"Repeated argument x to function repeated_arg (at line 44, column 14).">>, - <<"Repeated argument y to function repeated_arg (at line 44, column 14).">>, - <<"No record type with fields y, z (at line 14, column 24)">>, - <<"The field z is missing when constructing an element of type r2 (at line 15, column 26)">>, - <<"Record type r2 does not have field y (at line 15, column 24)">>, - <<"Let binding at line 47, column 5 must be followed by an expression">>, - <<"Let binding at line 50, column 5 must be followed by an expression">>, - <<"Let binding at line 54, column 5 must be followed by an expression">>, - <<"Let binding at line 58, column 5 must be followed by an expression">>]} + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>]} , {"init_type_error", - [<<"Cannot unify string\n" + [<>]} , {"missing_state_type", - [<<"Cannot unify string\n" + [<>]} , {"missing_fields_in_record_expression", - [<<"The field x is missing when constructing an element of type r('a) (at line 7, column 42)">>, - <<"The field y is missing when constructing an element of type r(int) (at line 8, column 42)">>, - <<"The fields y, z are missing when constructing an element of type r('a) (at line 6, column 42)">>]} + [<>, + <>, + <>]} , {"namespace_clash", - [<<"The contract Call (at line 4, column 10) has the same name as a namespace at (builtin location)">>]} + [<>]} , {"bad_events", - [<<"The indexed type string (at line 9, column 25) is not a word type">>, - <<"The indexed type alias_string (at line 10, column 25) equals string which is not a word type">>]} + [<>, + <>]} , {"bad_events2", - [<<"The event constructor BadEvent1 (at line 9, column 7) has too many non-indexed values (max 1)">>, - <<"The event constructor BadEvent2 (at line 10, column 7) has too many indexed values (max 3)">>]} + [<>, + <>]} , {"type_clash", - [<<"Cannot unify int\n" + [< Remote.themap\n" "against the expected type\n" " (gas : int, value : int) => map(string, int)">>]} , {"bad_include_and_ns", - [<<"Include of 'included.aes' at line 2, column 11\nnot allowed, include only allowed at top level.">>, - <<"Nested namespace not allowed\nNamespace 'Foo' at line 3, column 13 not defined at top level.">>]} + [<>, + <>]} , {"bad_address_literals", - [<<"The type bytes(32) is not a contract type\n" + [<>, - <<"The type oracle(int, bool) is not a contract type\n" + <>, - <<"The type address is not a contract type\n" + <>, - <<"Cannot unify oracle_query('a, 'b)\n" + <>, - <<"Cannot unify oracle_query('c, 'd)\n" + <>, - <<"Cannot unify oracle_query('e, 'f)\n" + <>, - <<"Cannot unify oracle('g, 'h)\n" + <>, - <<"Cannot unify oracle('i, 'j)\n" + <>, - <<"Cannot unify oracle('k, 'l)\n" + <>, - <<"Cannot unify address\n" + <>, - <<"Cannot unify address\n" + <>, - <<"Cannot unify address\n" + <>]} , {"stateful", - [<<"Cannot reference stateful function Chain.spend (at line 13, column 35)\nin the definition of non-stateful function fail1.">>, - <<"Cannot reference stateful function local_spend (at line 14, column 35)\nin the definition of non-stateful function fail2.">>, - <<"Cannot reference stateful function Chain.spend (at line 16, column 15)\nin the definition of non-stateful function fail3.">>, - <<"Cannot reference stateful function Chain.spend (at line 20, column 31)\nin the definition of non-stateful function fail4.">>, - <<"Cannot reference stateful function Chain.spend (at line 35, column 47)\nin the definition of non-stateful function fail5.">>, - <<"Cannot pass non-zero value argument 1000 (at line 48, column 57)\nin the definition of non-stateful function fail6.">>, - <<"Cannot pass non-zero value argument 1000 (at line 49, column 56)\nin the definition of non-stateful function fail7.">>, - <<"Cannot pass non-zero value argument 1000 (at line 52, column 17)\nin the definition of non-stateful function fail8.">>]} + [<>, + <>, + <>, + <>, + <>, + <>, + <>, + <>]} , {"bad_init_state_access", - [<<"The init function should return the initial state as its result and cannot write the state,\n" + [<>, - <<"The init function should return the initial state as its result and cannot read the state,\n" + <>, - <<"The init function should return the initial state as its result and cannot read the state,\n" + <>]} , {"field_parse_error", - [<<"Cannot use nested fields or keys in record construction: p.x">>]} + [<>]} , {"modifier_checks", - [<<"The function all_the_things (at line 11, column 3) cannot be both public and private.">>, - <<"Namespaces cannot contain entrypoints (at line 3, column 3). Use 'function' instead.">>, - <<"The contract Remote (at line 5, column 10) has no entrypoints. Since Sophia version 3.2, public\ncontract functions must be declared with the 'entrypoint' keyword instead of\n'function'.">>, - <<"The entrypoint wha (at line 12, column 3) cannot be private. Use 'function' instead.">>, - <<"Use 'entrypoint' for declaration of foo (at line 6, column 3):\n entrypoint foo : () => unit">>, - <<"Use 'entrypoint' instead of 'function' for public function foo (at line 10, column 3):\n entrypoint foo() = ()">>, - <<"Use 'entrypoint' instead of 'function' for public function foo (at line 6, column 3):\n entrypoint foo : () => unit">>]} + [<>, + <>, + <>, + <>, + < unit">>, + <>, + < unit">>]} , {"list_comp_not_a_list", - [<<"Cannot unify int\n and list('a)\nwhen checking rvalue of list comprehension binding at line 2, column 36\n 1 : int\nagainst type \n list('a)">> + [<> ]} , {"list_comp_if_not_bool", - [<<"Cannot unify int\n and bool\nwhen checking the type of the expression at line 2, column 44\n 3 : int\nagainst the expected type\n bool">> + [<> ]} , {"list_comp_bad_shadow", - [<<"Cannot unify int\n and string\nwhen checking the type of the pattern at line 2, column 53\n x : int\nagainst the expected type\n string">> + [<> ]} ]. +-define(Path(File), "code_errors/" ??File). +-define(Msg(File, Line, Col, Err), <>). + +-define(SAME(File, Line, Col, Err), {?Path(File), ?Msg(File, Line, Col, Err)}). +-define(AEVM(File, Line, Col, Err), {?Path(File), [{aevm, ?Msg(File, Line, Col, Err)}]}). +-define(FATE(File, Line, Col, Err), {?Path(File), [{fate, ?Msg(File, Line, Col, Err)}]}). +-define(BOTH(File, Line, Col, ErrAEVM, ErrFATE), + {?Path(File), [{aevm, ?Msg(File, Line, Col, ErrAEVM)}, + {fate, ?Msg(File, Line, Col, ErrFATE)}]}). + +failing_code_gen_contracts() -> + [ ?SAME(last_declaration_must_be_contract, 1, 1, + "Expected a contract as the last declaration instead of the namespace 'LastDeclarationIsNotAContract'") + , ?AEVM(polymorphic_entrypoint, 2, 17, + "The argument\n" + " x : 'a\n" + "of entrypoint 'id' does not have a monomorphic type.\n" + "Use the FATE backend if you want polymorphic entrypoints.") + , ?SAME(missing_init_function, 1, 10, + "Missing init function for the contract 'MissingInitFunction'.\n" + "The 'init' function can only be omitted if the state type is 'unit'.") + , ?SAME(parameterised_state, 3, 8, + "The state type cannot be parameterized.") + , ?SAME(parameterised_event, 3, 12, + "The event type cannot be parameterized.") + , ?SAME(polymorphic_aens_resolve, 4, 5, + "Invalid return type of AENS.resolve:\n" + " 'a\n" + "It must be a string or a pubkey type (address, oracle, etc).") + , ?SAME(bad_aens_resolve, 6, 5, + "Invalid return type of AENS.resolve:\n" + " list(int)\n" + "It must be a string or a pubkey type (address, oracle, etc).") + , ?AEVM(polymorphic_compare, 4, 5, + "Cannot compare values of type\n" + " 'a\n" + "The AEVM only supports '==' on values of\n" + "- word type (int, bool, bits, address, oracle(_, _), etc)\n" + "- type string\n" + "- tuple or record of word type\n" + "Use FATE if you need to compare arbitrary types.") + , ?AEVM(higher_order_compare, 4, 5, + "Cannot compare values of type\n" + " (int) => int\n" + "The AEVM only supports '<' on values of\n" + "- word type (int, bool, bits, address, oracle(_, _), etc)\n" + "Use FATE if you need to compare arbitrary types.") + ]. + diff --git a/test/aeso_parser_tests.erl b/test/aeso_parser_tests.erl index 7197bd3..6b2fc5c 100644 --- a/test/aeso_parser_tests.erl +++ b/test/aeso_parser_tests.erl @@ -39,7 +39,7 @@ simple_contracts_test_() -> RightAssoc = fun(Op) -> CheckParens({a, Op, {b, Op, c}}) end, NonAssoc = fun(Op) -> OpAtom = list_to_atom(Op), - ?assertThrow({parse_errors, [_]}, + ?assertThrow({error, [_]}, parse_expr(NoPar({a, Op, {b, Op, c}}))) end, Stronger = fun(Op1, Op2) -> CheckParens({{a, Op1, b}, Op2, c}), diff --git a/test/contracts/code_errors/bad_aens_resolve.aes b/test/contracts/code_errors/bad_aens_resolve.aes new file mode 100644 index 0000000..38d932b --- /dev/null +++ b/test/contracts/code_errors/bad_aens_resolve.aes @@ -0,0 +1,9 @@ +contract BadAENSresolve = + + type t('a) = option(list('a)) + + function fail() : t(int) = + AENS.resolve("foo.aet", "whatever") + + entrypoint main() = () + diff --git a/test/contracts/code_errors/higher_order_compare.aes b/test/contracts/code_errors/higher_order_compare.aes new file mode 100644 index 0000000..c60325d --- /dev/null +++ b/test/contracts/code_errors/higher_order_compare.aes @@ -0,0 +1,8 @@ +contract HigherOrderCompare = + + function cmp(x : int => int, y) : bool = + x < y + + entrypoint test() = + let f(x) = (y) => x + y + cmp(f(1), f(2)) diff --git a/test/contracts/code_errors/last_declaration_must_be_contract.aes b/test/contracts/code_errors/last_declaration_must_be_contract.aes new file mode 100644 index 0000000..1c72d81 --- /dev/null +++ b/test/contracts/code_errors/last_declaration_must_be_contract.aes @@ -0,0 +1,2 @@ +namespace LastDeclarationIsNotAContract = + function add(x, y) = x + y diff --git a/test/contracts/code_errors/missing_init_function.aes b/test/contracts/code_errors/missing_init_function.aes new file mode 100644 index 0000000..49372fb --- /dev/null +++ b/test/contracts/code_errors/missing_init_function.aes @@ -0,0 +1,3 @@ +contract MissingInitFunction = + type state = int * int + diff --git a/test/contracts/code_errors/parameterised_event.aes b/test/contracts/code_errors/parameterised_event.aes new file mode 100644 index 0000000..422f67b --- /dev/null +++ b/test/contracts/code_errors/parameterised_event.aes @@ -0,0 +1,4 @@ +contract ParameterisedEvent = + + datatype event('a) = Event(int) + diff --git a/test/contracts/code_errors/parameterised_state.aes b/test/contracts/code_errors/parameterised_state.aes new file mode 100644 index 0000000..fed262e --- /dev/null +++ b/test/contracts/code_errors/parameterised_state.aes @@ -0,0 +1,4 @@ +contract ParameterisedState = + + type state('a) = list('a) + diff --git a/test/contracts/code_errors/polymorphic_aens_resolve.aes b/test/contracts/code_errors/polymorphic_aens_resolve.aes new file mode 100644 index 0000000..6301743 --- /dev/null +++ b/test/contracts/code_errors/polymorphic_aens_resolve.aes @@ -0,0 +1,7 @@ +contract PolymorphicAENSresolve = + + function fail() : option('a) = + AENS.resolve("foo.aet", "whatever") + + entrypoint main() = () + diff --git a/test/contracts/code_errors/polymorphic_compare.aes b/test/contracts/code_errors/polymorphic_compare.aes new file mode 100644 index 0000000..91ba62e --- /dev/null +++ b/test/contracts/code_errors/polymorphic_compare.aes @@ -0,0 +1,7 @@ +contract PolymorphicCompare = + + function cmp(x : 'a, y : 'a) : bool = + x == y + + entrypoint test() = + cmp(4, 6) && cmp(true, false) diff --git a/test/contracts/code_errors/polymorphic_entrypoint.aes b/test/contracts/code_errors/polymorphic_entrypoint.aes new file mode 100644 index 0000000..d994a95 --- /dev/null +++ b/test/contracts/code_errors/polymorphic_entrypoint.aes @@ -0,0 +1,3 @@ +contract PolymorphicEntrypoint = + entrypoint id(x : 'a) : 'a = x +