diff --git a/CHANGELOG.md b/CHANGELOG.md index da38577..5aa5a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `main` keyword to indicate the main contract in case there are child contracts around - `List.sum` and `List.product` no longer use `List.foldl` ### Removed -- Singleton tuples are no longer unwrapped (this was causing problems with arity 1 init functions) ## [5.0.0] 2021-04-30 ### Added diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 12aeb71..bd52d1b 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -78,7 +78,8 @@ context :: {contract_literal, aeso_syntax:expr()} | {address_to_contract, aeso_syntax:ann()} | {bytecode_hash, aeso_syntax:ann()} | - {var_args, aeso_syntax:ann(), aeso_syntax:expr()} + {var_args, aeso_syntax:ann(), aeso_syntax:expr()}, + force_def = false :: boolean() }). -type field_constraint() :: #field_constraint{} | #record_create_constraint{} | #is_contract_constraint{}. @@ -754,10 +755,11 @@ infer(Contracts, Options) -> try Env = init_env(Options), create_options(Options), + ets_new(defined_contracts, [bag]), ets_new(type_vars, [set]), check_modifiers(Env, Contracts), create_type_errors(), - Contracts1 = identify_main_contract(Contracts), + Contracts1 = identify_main_contract(Contracts, Options), destroy_and_report_type_errors(Env), {Env1, Decls} = infer1(Env, Contracts1, [], Options), {Env2, DeclsFolded, DeclsUnfolded} = @@ -786,6 +788,10 @@ infer1(Env, [{Contract, Ann, ConName, Code} | Rest], Acc, Options) contract_child -> contract; contract_interface -> contract_interface end, + case What of + contract -> ets_insert(defined_contracts, {qname(ConName)}); + contract_interface -> ok + end, {Env1, Code1} = infer_contract_top(push_scope(contract, ConName, Env), What, Code, Options), Contract1 = {Contract, Ann, ConName, Code1}, Env2 = pop_scope(Env1), @@ -801,18 +807,22 @@ infer1(Env, [{pragma, _, _} | Rest], Acc, Options) -> infer1(Env, Rest, Acc, Options). %% Asserts that the main contract is somehow defined. -identify_main_contract(Contracts) -> +identify_main_contract(Contracts, Options) -> Children = [C || C = {contract_child, _, _, _} <- Contracts], Mains = [C || C = {contract_main, _, _, _} <- Contracts], case Mains of [] -> case Children of - [] -> type_error({main_contract_undefined}); + [] -> type_error( + {main_contract_undefined, + [{file, File} || {src_file, File} <- Options]}); [{contract_child, Ann, Con, Body}] -> (Contracts -- Children) ++ [{contract_main, Ann, Con, Body}]; - _ -> type_error({ambiguous_main_contract}) + [H|_] -> type_error({ambiguous_main_contract, + aeso_syntax:get_ann(H)}) end; [_] -> (Contracts -- Mains) ++ Mains; %% Move to the end - _ -> type_error({multiple_main_contracts}) + [H|_] -> type_error({multiple_main_contracts, + aeso_syntax:get_ann(H)}) end. check_scope_name_clash(Env, Kind, Name) -> @@ -1627,7 +1637,7 @@ infer_var_args_fun(Env, {typed, Ann, Fun, FunType0}, NamedArgs, ArgTypes) -> % generally type error, but will be caught _ -> [GasCapMock, ProtectedMock|NamedArgsT] end, - check_contract_construction(Env, RetT, Fun, NamedArgsT1, ArgTypes, RetT), + check_contract_construction(Env, true, RetT, Fun, NamedArgsT1, ArgTypes, RetT), {fun_t, Ann, NamedArgsT, ArgTypes, RetT}; {qid, _, ["Chain", "clone"]} -> {fun_t, _, NamedArgsT, var_args, RetT} = FunType0, @@ -1639,15 +1649,15 @@ infer_var_args_fun(Env, {typed, Ann, Fun, FunType0}, NamedArgs, ArgTypes) -> end, NamedArgsTNoRef = lists:filter(fun({named_arg_t, _, {id, _, "ref"}, _, _}) -> false; (_) -> true end, NamedArgsT), - check_contract_construction(Env, ContractT, Fun, NamedArgsTNoRef, ArgTypes, RetT), + check_contract_construction(Env, false, ContractT, Fun, NamedArgsTNoRef, ArgTypes, RetT), {fun_t, Ann, NamedArgsT, ArgTypes, {if_t, Ann, {id, Ann, "protected"}, {app_t, Ann, {id, Ann, "option"}, [RetT]}, RetT}}; _ -> FunType0 end, {typed, Ann, Fun, FunType}. --spec check_contract_construction(env(), utype(), utype(), named_args_t(), [utype()], utype()) -> ok. -check_contract_construction(Env, ContractT, Fun, NamedArgsT, ArgTypes, RetT) -> +-spec check_contract_construction(env(), boolean(), utype(), utype(), named_args_t(), [utype()], utype()) -> ok. +check_contract_construction(Env, ForceDef, ContractT, Fun, NamedArgsT, ArgTypes, RetT) -> Ann = aeso_syntax:get_ann(Fun), InitT = fresh_uvar(Ann), unify(Env, InitT, {fun_t, Ann, NamedArgsT, ArgTypes, fresh_uvar(Ann)}, {checking_init_args, Ann, ContractT, ArgTypes}), @@ -1660,7 +1670,9 @@ check_contract_construction(Env, ContractT, Fun, NamedArgsT, ArgTypes, RetT) -> kind = project, context = {var_args, Ann, Fun} } , #is_contract_constraint{ contract_t = ContractT, - context = {var_args, Ann, Fun} } + context = {var_args, Ann, Fun}, + force_def = ForceDef + } ]), ok. @@ -1836,7 +1848,7 @@ next_count() -> ets_tables() -> [options, type_vars, type_defs, record_fields, named_argument_constraints, - field_constraints, freshen_tvars, type_errors]. + field_constraints, freshen_tvars, type_errors, defined_contracts]. clean_up_ets() -> [ catch ets_delete(Tab) || Tab <- ets_tables() ], @@ -2110,12 +2122,20 @@ check_record_create_constraints(Env, [C | Cs]) -> end, check_record_create_constraints(Env, Cs). +is_contract_defined(C) -> + ets_lookup(defined_contracts, qname(C)) =/= []. + check_is_contract_constraints(_Env, []) -> ok; check_is_contract_constraints(Env, [C | Cs]) -> - #is_contract_constraint{ contract_t = Type, context = Cxt } = C, + #is_contract_constraint{ contract_t = Type, context = Cxt, force_def = ForceDef } = C, Type1 = unfold_types_in_type(Env, instantiate(Type)), - case lookup_type(Env, record_type_name(Type1)) of - {_, {_Ann, {[], {contract_t, _}}}} -> ok; + TypeName = record_type_name(Type1), + case lookup_type(Env, TypeName) of + {_, {_Ann, {[], {contract_t, _}}}} -> + case not ForceDef orelse is_contract_defined(TypeName) of + true -> ok; + false -> type_error({contract_lacks_definition, Type1, Cxt}) + end; _ -> type_error({not_a_contract_type, Type1, Cxt}) end, check_is_contract_constraints(Env, Cs). @@ -2943,15 +2963,15 @@ mk_error({named_argument_must_be_literal_bool, Name, Arg}) -> mk_error({conflicting_updates_for_field, Upd, Key}) -> Msg = io_lib:format("Conflicting updates for field '~s'\n", [Key]), mk_t_err(pos(Upd), Msg); -mk_error({ambiguous_main_contract}) -> +mk_error({ambiguous_main_contract, Ann}) -> Msg = "Could not deduce the main contract. You can point it out manually with the `main` keyword.", - mk_t_err(pos(0, 0), Msg); -mk_error({main_contract_undefined}) -> + mk_t_err(pos(Ann), Msg); +mk_error({main_contract_undefined, Ann}) -> Msg = "No contract defined.\n", - mk_t_err(pos(0, 0), Msg); -mk_error({multiple_main_contracts}) -> + mk_t_err(pos(Ann), Msg); +mk_error({multiple_main_contracts, Ann}) -> Msg = "Only one main contract can be defined.\n", - mk_t_err(pos(0, 0), Msg); + mk_t_err(pos(Ann), Msg); mk_error({unify_varargs, When}) -> Msg = "Cannot unify variable argument list.\n", {Pos, Ctxt} = pp_when(When), @@ -2959,6 +2979,13 @@ mk_error({unify_varargs, When}) -> mk_error({clone_no_contract, Ann}) -> Msg = "Chain.clone requires `ref` named argument of contract type.\n", mk_t_err(pos(Ann), Msg); +mk_error({contract_lacks_definition, Type, When}) -> + Msg = io_lib:format( + "~s is not implemented.\n", + [pp_type(Type)] + ), + {Pos, Ctxt} = pp_when(When), + mk_t_err(Pos, Msg, Ctxt); mk_error(Err) -> Msg = io_lib:format("Unknown error: ~p\n", [Err]), mk_t_err(pos(0, 0), Msg). diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 83282d1..00df6ac 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -34,7 +34,7 @@ simple_compile_test_() -> #{fate_code := Code} when Backend == fate -> Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), ?assertMatch({X, X}, {Code1, Code}); - Error -> print_and_throw(Error) + Error -> io:format("\n\n~p\n\n", [Error]), print_and_throw(Error) end end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], not lists:member(ContractName, not_compilable_on(Backend))] ++ @@ -126,8 +126,7 @@ compile(Backend, Name, Options) -> %% The currently compilable contracts. compilable_contracts() -> - ["test", - "complex_types", + ["complex_types", "counter", "dutch_auction", "environment", @@ -180,7 +179,10 @@ compilable_contracts() -> "protected_call", "hermetization_turnoff", "multiple_contracts", - "clone", "clone_simple", "create" + "clone", + "clone_simple", + "create", + "test" % Custom general-purpose test file. Keep it last on the list. ]. not_compilable_on(fate) -> []; @@ -728,6 +730,39 @@ failing_contracts() -> , ?TYPE_ERROR(bad_state, [<>]) + , ?TYPE_ERROR(factories_type_errors, + [<>, + < if(protected, option(void), void)\n and (gas : int, value : int, protected : bool, int, bool) => 'b\n" + "when checking contract construction of type\n (gas : int, value : int, protected : bool) =>\n if(protected, option(void), void) (at line 11, column 18)\nagainst the expected type\n (gas : int, value : int, protected : bool, int, bool) => 'b">>, + <>, + <>, + < if(protected, option(void), void)\n and (gas : int, value : int, protected : bool) => 'a\n" + "when checking contract construction of type\n (gas : int, value : int, protected : bool, int, bool) =>\n if(protected, option(void), void) (at line 18, column 18)\nagainst the expected type\n (gas : int, value : int, protected : bool) => 'a">>, + <>, + <> + ]) + , ?TYPE_ERROR(ambiguous_main, + [<> + ]) + , ?TYPE_ERROR(no_main_contract, + [<> + ]) + , ?TYPE_ERROR(multiple_main_contracts, + [<> + ]) ]. -define(Path(File), "code_errors/" ??File). @@ -839,6 +874,8 @@ failing_code_gen_contracts() -> "Invalid state type\n" " {f : (int) => int}\n" "The state cannot contain functions in the AEVM. Use FATE if you need this.") + , ?FATE(child_with_decls, 2, 14, + "Missing definition of function 'f'.") ]. validation_test_() -> diff --git a/test/contracts/ambiguous_main.aes b/test/contracts/ambiguous_main.aes new file mode 100644 index 0000000..d375fa9 --- /dev/null +++ b/test/contracts/ambiguous_main.aes @@ -0,0 +1,5 @@ +contract C = + entrypoint f() = 123 + +contract D = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/code_errors/child_with_decls.aes b/test/contracts/code_errors/child_with_decls.aes new file mode 100644 index 0000000..4b2d469 --- /dev/null +++ b/test/contracts/code_errors/child_with_decls.aes @@ -0,0 +1,5 @@ +contract C = + entrypoint f : () => unit + +main contract M = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/factories_type_errors.aes b/test/contracts/factories_type_errors.aes new file mode 100644 index 0000000..2a5ea00 --- /dev/null +++ b/test/contracts/factories_type_errors.aes @@ -0,0 +1,24 @@ +contract interface Kaboom = + entrypoint init : () => void + +contract Bakoom = + type state = int + entrypoint init(x, b) = if(b) x else 0 + +main contract Test = + stateful entrypoint test(k : Kaboom) = + let k_bad1 = Chain.clone() : Kaboom + let k_bad2 = Chain.clone(ref=k, 123, true) + let k_bad3 = Chain.clone(ref=k, gas=true) + let k_bad4 = Chain.create() : Kaboom + let k_gud1 = Chain.clone(ref=k) + let Some(k_gud2) = Chain.clone(ref=k, protected=true) + let Some(k_gud3) = Chain.clone(ref=k, value=10, protected=true, gas=123) + + let b_bad1 = Chain.create() : Bakoom + let b_bad2 = Chain.create(123, true, protected=true) : Bakoom + let b_bad3 = Chain.create(123, true, value=true) : Bakoom + let b_gud1 = Chain.create(123, true) : Bakoom + let b_gud2 = Chain.create(123, true, value=100) : Bakoom + + b_gud1 \ No newline at end of file diff --git a/test/contracts/multiple_main_contracts.aes b/test/contracts/multiple_main_contracts.aes new file mode 100644 index 0000000..6c85bb1 --- /dev/null +++ b/test/contracts/multiple_main_contracts.aes @@ -0,0 +1,5 @@ +main contract C = + entrypoint f() = 123 + +main contract D = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/no_main_contract.aes b/test/contracts/no_main_contract.aes new file mode 100644 index 0000000..4b0e9b4 --- /dev/null +++ b/test/contracts/no_main_contract.aes @@ -0,0 +1,2 @@ +contract interface C = + entrypoint f : () => unit \ No newline at end of file