diff --git a/rebar.config b/rebar.config index 8d9c9aa..036b2cf 100644 --- a/rebar.config +++ b/rebar.config @@ -2,7 +2,7 @@ {erl_opts, [debug_info]}. -{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"5e16b85"}}} +{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"a533fd5"}}} , {getopt, "1.0.1"} , {eblake2, "1.0.0"} , {jsx, {git, "https://github.com/talentdeficit/jsx.git", diff --git a/rebar.lock b/rebar.lock index 03d8ad6..db0d1a7 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,7 +1,7 @@ {"1.1.0", [{<<"aebytecode">>, {git,"https://github.com/aeternity/aebytecode.git", - {ref,"5e16b85ae2385bdc010f319e2f2717c09cac3621"}}, + {ref,"a533fd5fcb25d369caeef4dac95d79941c51620f"}}, 0}, {<<"aeserialization">>, {git,"https://github.com/aeternity/aeserialization.git", diff --git a/src/aeso_code_errors.erl b/src/aeso_code_errors.erl index b178637..05bc135 100644 --- a/src/aeso_code_errors.erl +++ b/src/aeso_code_errors.erl @@ -8,7 +8,7 @@ %%%------------------------------------------------------------------- -module(aeso_code_errors). --export([format/1]). +-export([format/1, pos/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", diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 3ee0d48..d0e1181 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -65,18 +65,19 @@ version() -> {ok, list_to_binary(VsnString)} end. --spec file(string()) -> {ok, map()} | {error, binary()}. +-spec file(string()) -> {ok, map()} | {error, [aeso_errors:error()]}. file(Filename) -> file(Filename, []). --spec file(string(), options()) -> {ok, map()} | {error, binary()}. +-spec file(string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}. file(File, Options0) -> Options = add_include_path(File, Options0), case read_contract(File) of {ok, Bin} -> from_string(Bin, [{src_file, File} | Options]); {error, Error} -> - ErrorString = [File,": ",file:format_error(Error)], - {error, join_errors("File errors", [ErrorString], fun(E) -> E end)} + Msg = lists:flatten([File,": ",file:format_error(Error)]), + Pos = aeso_errors:pos(0, 0), + {error, [aeso_errors:new(file_error, Pos, Msg)]} end. add_include_path(File, Options) -> @@ -88,7 +89,7 @@ add_include_path(File, Options) -> [{include, {file_system, [Cwd, Dir]}} | Options] end. --spec from_string(binary() | string(), options()) -> {ok, map()} | {error, binary()}. +-spec from_string(binary() | string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}. from_string(Contract, Options) -> from_string(proplists:get_value(backend, Options, aevm), Contract, Options). @@ -152,10 +153,6 @@ string_to_code(ContractString, Options) -> type_env => TypeEnv} end. -join_errors(Prefix, Errors, Pfun) -> - Ess = [ Pfun(E) || E <- Errors ], - list_to_binary(string:join([Prefix|Ess], "\n")). - -define(CALL_NAME, "__call"). -define(DECODE_NAME, "__decode"). @@ -167,7 +164,7 @@ join_errors(Prefix, Errors, Pfun) -> %% a special return type (typerep, T) -spec check_call(string(), string(), [string()], options()) -> {ok, string(), {[Type], Type}, [term()]} | {ok, string(), [term()]} - | {error, term()} + | {error, [aeso_errors:error()]} when Type :: term(). check_call(Source, "init" = FunName, Args, Options) -> case check_call1(Source, FunName, Args, Options) of @@ -222,10 +219,7 @@ check_call1(ContractString0, FunName, Args, Options) -> {ok, FunName, CallArgs} end catch - 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, Errors} -> {error, Errors} end. arguments_of_body(CallName, _FunName, Fcode) -> @@ -273,24 +267,31 @@ last_contract_indent(Decls) -> end. -spec to_sophia_value(string(), string(), ok | error | revert, aeb_aevm_data:data()) -> - {ok, aeso_syntax:expr()} | {error, term()}. + {ok, aeso_syntax:expr()} | {error, [aeso_errors:error()]}. to_sophia_value(ContractString, Fun, ResType, Data) -> to_sophia_value(ContractString, Fun, ResType, Data, [{backend, aevm}]). -spec to_sophia_value(string(), string(), ok | error | revert, binary(), options()) -> - {ok, aeso_syntax:expr()} | {error, term()}. + {ok, aeso_syntax:expr()} | {error, [aeso_errors:error()]}. to_sophia_value(_, _, error, Err, _Options) -> {ok, {app, [], {id, [], "error"}, [{string, [], Err}]}}; to_sophia_value(_, _, revert, Data, Options) -> case proplists:get_value(backend, Options, aevm) of aevm -> case aeb_heap:from_binary(string, Data) of - {ok, Err} -> {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}}; - {error, _} = Err -> Err + {ok, Err} -> + {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}}; + {error, _} -> + Msg = "Could not interpret the revert message\n", + {error, [aeso_errors:new(data_error, aeso_errors:pos(0, 0), Msg)]} end; fate -> - Err = aeb_fate_encoding:deserialize(Data), - {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}} + try aeb_fate_encoding:deserialize(Data) of + Err -> {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}} + catch _:_ -> + Msg = "Could not deserialize the revert message\n", + {error, [aeso_errors:new(data_error, aeso_errors:pos(0, 0), Msg)]} + end end; to_sophia_value(ContractString, FunName, ok, Data, Options0) -> Options = [no_code | Options0], @@ -310,43 +311,41 @@ to_sophia_value(ContractString, FunName, ok, Data, Options0) -> {ok, aeso_vm_decode:from_aevm(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)} + Msg = io_lib:format("Cannot translate VM value ~p\n of type ~p\n to Sophia type ~s\n", + [Data, VmType, Type0Str]), + {error, [aeso_errors:new(type_error, aeso_errors:pos(0, 0), Msg)]} 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)} + Msg = io_lib:format("Failed to decode binary as type ~p\n", [VmType]), + {error, [aeso_errors:new(code_error, aeso_errors:pos(0, 0), Msg)]} end; fate -> - try - {ok, aeso_vm_decode:from_fate(Type, aeb_fate_encoding:deserialize(Data))} - catch throw:cannot_translate_to_sophia -> - {error, join_errors("Translation error", - [lists:flatten(io_lib:format("Cannot translate fate value ~p\n of Sophia type ~s\n", - [aeb_fate_encoding:deserialize(Data), Type]))], - fun (E) -> E end)}; - _:R -> - {error, iolist_to_binary(io_lib:format("Decode error ~p: ~p\n", [R, erlang:get_stacktrace()]))} + try + {ok, aeso_vm_decode:from_fate(Type, aeb_fate_encoding:deserialize(Data))} + catch throw:cannot_translate_to_sophia -> + Type1 = prettypr:format(aeso_pretty:type(Type)), + Msg = io_lib:format("Cannot translate FATE value ~p\n of Sophia type ~s\n", + [aeb_fate_encoding:deserialize(Data), Type1]), + {error, [aeso_errors:new(type_error, aeso_errors:pos(0, 0), Msg)]}; + _:_ -> + Type1 = prettypr:format(aeso_pretty:type(Type)), + Msg = io_lib:format("Failed to decode binary as type ~s\n", [Type1]), + {error, [aeso_errors:new(code_error, aeso_errors:pos(0, 0), Msg)]} end end catch - throw:{error, Errors} -> {error, Errors}; - error:{badmatch, {error, missing_function}} -> - {error, join_errors("Type errors", ["no function: '" ++ FunName ++ "'"], - fun (E) -> E end)} + throw:{error, Errors} -> {error, Errors} end. -spec create_calldata(string(), string(), [string()]) -> {ok, binary(), aeb_aevm_data:type(), aeb_aevm_data:type()} - | {error, term()}. + | {error, [aeso_errors:error()]}. create_calldata(Code, Fun, Args) -> create_calldata(Code, Fun, Args, [{backend, aevm}]). -spec create_calldata(string(), string(), [string()], [{atom(), any()}]) -> - {ok, binary()} - | {error, term()}. + {ok, binary()} | {error, [aeso_errors:error()]}. create_calldata(Code, Fun, Args, Options0) -> Options = [no_code | Options0], case proplists:get_value(backend, Options, aevm) of @@ -366,7 +365,7 @@ create_calldata(Code, Fun, Args, Options0) -> -spec decode_calldata(string(), string(), binary()) -> {ok, [aeso_syntax:type()], [aeso_syntax:expr()]} - | {error, term()}. + | {error, [aeso_errors:error()]}. decode_calldata(ContractString, FunName, Calldata) -> decode_calldata(ContractString, FunName, Calldata, [{backend, aevm}]). @@ -393,15 +392,14 @@ decode_calldata(ContractString, FunName, Calldata, Options0) -> %% Values are Sophia expressions in AST format {ok, ArgTypes, Values} 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", - [VmValue, VmType, Type0Str]))], - fun (E) -> E end)} + Type0Str = prettypr:format(aeso_pretty:type(Type0)), + Msg = io_lib:format("Cannot translate VM value ~p\n of type ~p\n to Sophia type ~s\n", + [VmValue, VmType, Type0Str]), + {error, [aeso_errors:new(type_error, aeso_errors:pos(0, 0), Msg)]} 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)} + Msg = io_lib:format("Failed to decode calldata as type ~p\n", [VmType]), + {error, [aeso_errors:new(code_error, aeso_errors:pos(0, 0), Msg)]} end; fate -> case aeb_fate_abi:decode_calldata(FunName, Calldata) of @@ -413,30 +411,31 @@ decode_calldata(ContractString, FunName, Calldata, Options0) -> {ok, ArgTypes, AstArgs} 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 fate value ~p\n of Sophia type ~s\n", - [FateArgs, Type0Str]))], - fun (E) -> E end)} + Msg = io_lib:format("Cannot translate FATE value ~p\n of Sophia type ~s\n", + [FateArgs, Type0Str]), + {error, [aeso_errors:new(type_error, aeso_errors:pos(0, 0), Msg)]} end; {error, _} -> - {error, join_errors("Decode errors", ["Failed to decode binary"], - fun(E) -> E end)} + Msg = io_lib:format("Failed to decode calldata binary\n", []), + {error, [aeso_errors:new(code_error, aeso_errors:pos(0, 0), Msg)]} end end catch - throw:{error, Errors} -> - {error, Errors}; - error:{badmatch, {error, missing_function}} -> - {error, join_errors("Type errors", ["no function: '" ++ FunName ++ "'"], - fun (E) -> E end)} + throw:{error, Errors} -> {error, Errors} end. get_arg_icode(Funs) -> case [ Args || {[_, ?CALL_NAME], _, _, {funcall, _, Args}, _} <- Funs ] of [Args] -> Args; - [] -> error({missing_call_function, Funs}) + [] -> error_missing_call_function() end. +-dialyzer({nowarn_function, error_missing_call_function/0}). +error_missing_call_function() -> + Msg = "Internal error: missing '__call'-function", + Pos = aeso_errors:pos(0, 0), + aeso_errors:throw(aeso_errors:new(internal_error, Pos, Msg)). + get_call_type([{contract, _, _, Defs}]) -> case [ {lists:last(QFunName), FunType} || {letfun, _, {id, _, ?CALL_NAME}, [], _Ret, @@ -444,13 +443,14 @@ get_call_type([{contract, _, _, Defs}]) -> {app, _, {typed, _, {qid, _, QFunName}, FunType}, _}, _}} <- Defs ] of [Call] -> {ok, Call}; - [] -> {error, missing_call_function} + [] -> error_missing_call_function() end; get_call_type([_ | Contracts]) -> %% The __call should be in the final contract get_call_type(Contracts). -get_decode_type(FunName, [{contract, _, _, Defs}]) -> +-dialyzer({nowarn_function, get_decode_type/2}). +get_decode_type(FunName, [{contract, Ann, _, Defs}]) -> GetType = fun({letfun, _, {id, _, Name}, Args, Ret, _}) when Name == FunName -> [{Args, Ret}]; ({fun_decl, _, {id, _, Name}, {fun_t, _, _, Args, Ret}}) when Name == FunName -> [{Args, Ret}]; (_) -> [] end, @@ -459,7 +459,10 @@ get_decode_type(FunName, [{contract, _, _, Defs}]) -> [] -> case FunName of "init" -> {ok, [], {tuple_t, [], []}}; - _ -> {error, missing_function} + _ -> + Msg = io_lib:format("Function '~s' is missing in contract\n", [FunName]), + Pos = aeso_code_errors:pos(Ann), + aeso_errors:throw(aeso_errors:new(code_error, Pos, Msg)) end end; get_decode_type(FunName, [_ | Contracts]) -> diff --git a/src/aeso_errors.erl b/src/aeso_errors.erl index d22c56e..891c06b 100644 --- a/src/aeso_errors.erl +++ b/src/aeso_errors.erl @@ -15,7 +15,8 @@ }). -type pos() :: #pos{}. --type error_type() :: type_error | parse_error | code_error | internal_error. +-type error_type() :: type_error | parse_error | code_error + | file_error | data_error | internal_error. -record(err, { pos = #pos{} :: pos() , type :: error_type() @@ -71,9 +72,11 @@ str_pos(#pos{file = F, line = L, col = C}) -> type(#err{ type = Type }) -> Type. pp(#err{ pos = Pos } = Err) -> - lists:flatten(io_lib:format("~s\n~s", [pp_pos(Pos), msg(Err)])). + lists:flatten(io_lib:format("~s~s", [pp_pos(Pos), msg(Err)])). +pp_pos(#pos{file = no_file, line = 0, col = 0}) -> + ""; pp_pos(#pos{file = no_file, line = L, col = C}) -> - io_lib:format("At line ~p, col ~p:", [L, C]); + io_lib:format("At line ~p, col ~p:\n", [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:\n", [F, L, C]). diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index 8c585e0..46f1268 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -72,6 +72,58 @@ encode_decode_sophia_test() -> ok = Check("r", "{x = (\"foo\", 0), y = Red}"), ok. +to_sophia_value_neg_test() -> + Code = [ "contract Foo =\n" + " entrypoint x(y : int) : string = \"hello\"\n" ], + + {error, [Err1]} = aeso_compiler:to_sophia_value(Code, "x", ok, encode(12)), + ?assertEqual("Failed to decode binary as type string\n", aeso_errors:pp(Err1)), + {error, [Err2]} = aeso_compiler:to_sophia_value(Code, "x", ok, encode(12), [{backend, fate}]), + ?assertEqual("Failed to decode binary as type string\n", aeso_errors:pp(Err2)), + + {error, [Err3]} = aeso_compiler:to_sophia_value(Code, "x", revert, encode(12)), + ?assertEqual("Could not interpret the revert message\n", aeso_errors:pp(Err3)), + {error, [Err4]} = aeso_compiler:to_sophia_value(Code, "x", revert, encode(12), [{backend, fate}]), + ?assertEqual("Could not deserialize the revert message\n", aeso_errors:pp(Err4)), + ok. + +encode_calldata_neg_test() -> + Code = [ "contract Foo =\n" + " entrypoint x(y : int) : string = \"hello\"\n" ], + + ExpErr1 = "At line 5, col 34:\nCannot unify int\n and bool\n" + "when checking the application at line 5, column 34 of\n" + " x : (int) => string\nto arguments\n true : bool\n", + {error, [Err1]} = aeso_compiler:create_calldata(Code, "x", ["true"]), + ?assertEqual(ExpErr1, aeso_errors:pp(Err1)), + {error, [Err2]} = aeso_compiler:create_calldata(Code, "x", ["true"], [{backend, fate}]), + ?assertEqual(ExpErr1, aeso_errors:pp(Err2)), + + ok. + +decode_calldata_neg_test() -> + Code1 = [ "contract Foo =\n" + " entrypoint x(y : int) : string = \"hello\"\n" ], + Code2 = [ "contract Foo =\n" + " entrypoint x(y : string) : int = 42\n" ], + + {ok, CallDataAEVM} = aeso_compiler:create_calldata(Code1, "x", ["42"]), + {ok, CallDataFATE} = aeso_compiler:create_calldata(Code1, "x", ["42"], [{backend, fate}]), + + {error, [Err1]} = aeso_compiler:decode_calldata(Code2, "x", CallDataAEVM), + ?assertEqual("Failed to decode calldata as type {tuple,[string]}\n", aeso_errors:pp(Err1)), + {error, [Err2]} = aeso_compiler:decode_calldata(Code2, "x", <<1,2,3>>, [{backend, fate}]), + ?assertEqual("Failed to decode calldata binary\n", aeso_errors:pp(Err2)), + {error, [Err3]} = aeso_compiler:decode_calldata(Code2, "x", CallDataFATE, [{backend, fate}]), + ?assertEqual("Cannot translate FATE value \"*\"\n of Sophia type (string)\n", aeso_errors:pp(Err3)), + + {error, [Err4]} = aeso_compiler:decode_calldata(Code2, "y", CallDataAEVM), + ?assertEqual("At line 1, col 1:\nFunction 'y' is missing in contract\n", aeso_errors:pp(Err4)), + {error, [Err5]} = aeso_compiler:decode_calldata(Code2, "y", CallDataFATE, [{backend, fate}]), + ?assertEqual("At line 1, col 1:\nFunction 'y' is missing in contract\n", aeso_errors:pp(Err5)), + ok. + + encode_decode_sophia_string(SophiaType, String) -> io:format("String ~p~n", [String]), Code = [ "contract MakeCall =\n" diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index cb878a4..450ff36 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -32,6 +32,12 @@ simple_compile_test_() -> end end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], not lists:member(ContractName, not_yet_compilable(Backend))] ++ + [ {"Test file not found error", + fun() -> + {error, Errors} = aeso_compiler:file("does_not_exist.aes"), + ExpErr = <<"does_not_exist.aes: no such file or directory">>, + check_errors([ExpErr], Errors) + end} ] ++ [ {"Testing error messages of " ++ ContractName, fun() -> Errors = compile(aevm, ContractName),