From 256df25af4274dc5daadb36b252460b1be39a747 Mon Sep 17 00:00:00 2001 From: Gaith Hallak Date: Thu, 17 Nov 2022 11:40:57 +0300 Subject: [PATCH] Check contracts and entrypoints modifiers when implementing interfaces (#427) * Check contracts and entrypoints modifiers when implementing interfaces * Fix existing tests * Add passing tests * Add failing tests * Update docs * Update CHANGELOG --- CHANGELOG.md | 1 + docs/sophia_features.md | 11 +++ src/aeso_ast_infer_types.erl | 86 ++++++++++++++++--- test/aeso_compiler_tests.erl | 25 +++++- .../polymorphism_add_stateful_entrypoint.aes | 5 ++ ...morphism_change_entrypoint_to_function.aes | 6 ++ ...non_payable_contract_implement_payable.aes | 5 ++ ...on_payable_interface_implement_payable.aes | 8 ++ ...phism_preserve_or_add_payable_contract.aes | 14 +++ ...ism_preserve_or_add_payable_entrypoint.aes | 7 ++ ...preserve_or_remove_stateful_entrypoint.aes | 7 ++ ...polymorphism_remove_payable_entrypoint.aes | 5 ++ 12 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 test/contracts/polymorphism_add_stateful_entrypoint.aes create mode 100644 test/contracts/polymorphism_change_entrypoint_to_function.aes create mode 100644 test/contracts/polymorphism_non_payable_contract_implement_payable.aes create mode 100644 test/contracts/polymorphism_non_payable_interface_implement_payable.aes create mode 100644 test/contracts/polymorphism_preserve_or_add_payable_contract.aes create mode 100644 test/contracts/polymorphism_preserve_or_add_payable_entrypoint.aes create mode 100644 test/contracts/polymorphism_preserve_or_remove_stateful_entrypoint.aes create mode 100644 test/contracts/polymorphism_remove_payable_entrypoint.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9e081..9f929b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Return a mapping from variables to FATE registers in the compilation output. ### Changed - Type definitions serialised to ACI as `typedefs` field instead of `type_defs` to increase compatibility. +- Check contracts and entrypoints modifiers when implementing interfaces. ### Removed ### Fixed - Typechecker crashes if Chain.create or Chain.clone are used without arguments. diff --git a/docs/sophia_features.md b/docs/sophia_features.md index 68ac6fa..973dc22 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -191,6 +191,17 @@ contract interface X : Z = entrypoint z() = 1 ``` +#### Adding or removing modifiers + +When a `contract` or a `contract interface` implements another `contract interface`, the `payable` and `stateful` modifiers can be kept or changed, both in the contract and in the entrypoints, according to the following rules: + +1. A `payable` contract or interface can implement a `payable` interface or a non-`payable` interface. +2. A non-`payable` contract or interface can only implement a non-`payable` interface, and cannot implement a `payable` interface. +3. A `payable` entrypoint can implement a `payable` entrypoint or a non-`payable` entrypoint. +4. A non-`payable` entrypoint can only implement a non-`payable` entrypoint, and cannot implement a `payable` entrypoint. +5. A non-`stateful` entrypoint can implement a `stateful` entrypoint or a non-`stateful` entrypoint. +6. A `stateful` entrypoint can only implement a `stateful` entrypoint, and cannot implement a non-`stateful` entrypoint. + #### Subtyping and variance Subtyping in Sophia follows common rules that take type variance into account. As described by [Wikipedia](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)), diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 4a8c8cb..7b2a399 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -906,6 +906,7 @@ infer1(Env0, [Contract0 = {Contract, Ann, ConName, Impls, Code} | Rest], Acc, Op contract -> ets_insert(defined_contracts, {qname(ConName)}); contract_interface -> ok end, + check_contract_preserved_payability(Env, ConName, Ann, Impls, Acc, What), populate_functions_to_implement(Env, ConName, Impls, Acc), Env1 = bind_contract(untyped, Contract0, Env), {Env2, Code1} = infer_contract_top(push_scope(contract, ConName, Env1), What, Code, Options), @@ -931,6 +932,25 @@ infer1(Env, [{pragma, _, _} | Rest], Acc, Options) -> %% Pragmas are checked in check_modifiers infer1(Env, Rest, Acc, Options). +-spec check_contract_preserved_payability(env(), Con, Ann, Impls, Contracts, Kind) -> ok | no_return() when + Con :: aeso_syntax:con(), + Ann :: aeso_syntax:ann(), + Impls :: [Con], + Contracts :: [aeso_syntax:decl()], + Kind :: contract | contract_interface. +check_contract_preserved_payability(Env, ContractName, ContractAnn, Impls, DefinedContracts, Kind) -> + Payable = proplists:get_value(payable, ContractAnn, false), + ImplsNames = [ name(I) || I <- Impls ], + Interfaces = [ Con || I = {contract_interface, _, Con, _, _} <- DefinedContracts, + lists:member(name(Con), ImplsNames), + aeso_syntax:get_ann(payable, I, false) ], + + create_type_errors(), + [ type_error({unpreserved_payablity, Kind, ContractName, I}) || I <- Interfaces, Payable == false ], + destroy_and_report_type_errors(Env), + + ok. + %% Report all functions that were not implemented by the contract ContractName. -spec report_unimplemented_functions(env(), ContractName) -> ok | no_return() when ContractName :: aeso_syntax:con(). @@ -1487,19 +1507,37 @@ check_reserved_entrypoints(Funs) -> check_fundecl(Env, {fun_decl, Ann, Id = {id, _, Name}, Type = {fun_t, _, _, _, _}}) -> Type1 = {fun_t, _, Named, Args, Ret} = check_type(Env, Type), TypeSig = {type_sig, Ann, none, Named, Args, Ret}, - register_implementation(Name), + register_implementation(Id, TypeSig), {{Name, TypeSig}, {fun_decl, Ann, Id, Type1}}; check_fundecl(Env, {fun_decl, Ann, Id = {id, _, Name}, Type}) -> type_error({fundecl_must_have_funtype, Ann, Id, Type}), {{Name, {type_sig, Ann, none, [], [], Type}}, check_type(Env, Type)}. -%% Register the function FunName as implemented by deleting it from the functions +%% Register the function FunId as implemented by deleting it from the functions %% to be implemented table if it is included there, or return true otherwise. --spec register_implementation(FunName) -> true | no_return() when - FunName :: string(). -register_implementation(Name) -> +-spec register_implementation(FunId, FunSig) -> true | no_return() when + FunId :: aeso_syntax:id(), + FunSig :: typesig(). +register_implementation(Id, Sig) -> + Name = name(Id), case ets_lookup(functions_to_implement, Name) of - [{Name, _, {fun_decl, _, _, _}}] -> + [{Name, Interface, Decl = {fun_decl, _, DeclId, _}}] -> + DeclStateful = aeso_syntax:get_ann(stateful, Decl, false), + DeclPayable = aeso_syntax:get_ann(payable, Decl, false), + + SigEntrypoint = aeso_syntax:get_ann(entrypoint, Sig, false), + SigStateful = aeso_syntax:get_ann(stateful, Sig, false), + SigPayable = aeso_syntax:get_ann(payable, Sig, false), + + [ type_error({function_should_be_entrypoint, Id, DeclId, Interface}) + || not SigEntrypoint ], + + [ type_error({entrypoint_cannot_be_stateful, Id, DeclId, Interface}) + || SigStateful andalso not DeclStateful ], + + [ type_error({entrypoint_must_be_payable, Id, DeclId, Interface}) + || not SigPayable andalso DeclPayable ], + ets_delete(functions_to_implement, Name); [] -> true; @@ -1509,9 +1547,9 @@ register_implementation(Name) -> infer_nonrec(Env, LetFun) -> create_constraints(), - NewLetFun = {{FunName, _}, _} = infer_letfun(Env, LetFun), + NewLetFun = {{_, Sig}, _} = infer_letfun(Env, LetFun), check_special_funs(Env, NewLetFun), - register_implementation(FunName), + register_implementation(get_letfun_id(LetFun), Sig), solve_then_destroy_and_report_unsolved_constraints(Env), Result = {TypeSig, _} = instantiate(NewLetFun), print_typesig(TypeSig), @@ -1540,8 +1578,8 @@ infer_letrec(Env, Defs) -> ExtendEnv = bind_funs(Funs, Env), Inferred = [ begin - Res = {{Name, TypeSig}, _} = infer_letfun(ExtendEnv, LF), - register_implementation(Name), + Res = {{Name, TypeSig}, LetFun} = infer_letfun(ExtendEnv, LF), + register_implementation(get_letfun_id(LetFun), TypeSig), Got = proplists:get_value(Name, Funs), Expect = typesig_to_fun_t(TypeSig), unify(Env, Got, Expect, {check_typesig, Name, Got, Expect}), @@ -1593,6 +1631,9 @@ infer_letfun1(Env0 = #env{ namespace = NS }, {letfun, Attrib, Fun = {id, NameAtt {{Name, TypeSig}, {letfun, Attrib, {id, NameAttrib, Name}, TypedArgs, ResultType, NewGuardedBodies}}. +get_letfun_id({fun_clauses, _, Id, _, _}) -> Id; +get_letfun_id({letfun, _, Id, _, _, _}) -> Id. + desugar_clauses(Ann, Fun, {type_sig, _, _, _, ArgTypes, RetType}, Clauses) -> NeedDesugar = case Clauses of @@ -3659,7 +3700,7 @@ mk_error({empty_record_definition, Ann, Name}) -> Msg = io_lib:format("Empty record definitions are not allowed. Cannot define the record `~s`", [Name]), mk_t_err(pos(Ann), Msg); mk_error({unimplemented_interface_function, ConId, InterfaceName, FunName}) -> - Msg = io_lib:format("Unimplemented function `~s` from the interface `~s` in the contract `~s`", [FunName, InterfaceName, pp(ConId)]), + Msg = io_lib:format("Unimplemented entrypoint `~s` from the interface `~s` in the contract `~s`", [FunName, InterfaceName, pp(ConId)]), mk_t_err(pos(ConId), Msg); mk_error({referencing_undefined_interface, InterfaceId}) -> Msg = io_lib:format("Trying to implement or extend an undefined interface `~s`", [pp(InterfaceId)]), @@ -3707,6 +3748,29 @@ mk_error({interface_implementation_conflict, Contract, I1, I2, Fun}) -> "the contract `~s` have a function called `~s`", [name(I1), name(I2), name(Contract), name(Fun)]), mk_t_err(pos(Contract), Msg); +mk_error({function_should_be_entrypoint, Impl, Base, Interface}) -> + Msg = io_lib:format("`~s` must be declared as an entrypoint instead of a function " + "in order to implement the entrypoint `~s` from the interface `~s`", + [name(Impl), name(Base), name(Interface)]), + mk_t_err(pos(Impl), Msg); +mk_error({entrypoint_cannot_be_stateful, Impl, Base, Interface}) -> + Msg = io_lib:format("`~s` cannot be stateful because the entrypoint `~s` in the " + "interface `~s` is not stateful", + [name(Impl), name(Base), name(Interface)]), + mk_t_err(pos(Impl), Msg); +mk_error({entrypoint_must_be_payable, Impl, Base, Interface}) -> + Msg = io_lib:format("`~s` must be payable because the entrypoint `~s` in the " + "interface `~s` is payable", + [name(Impl), name(Base), name(Interface)]), + mk_t_err(pos(Impl), Msg); +mk_error({unpreserved_payablity, Kind, ContractCon, InterfaceCon}) -> + KindStr = case Kind of + contract -> "contract"; + contract_interface -> "interface" + end, + Msg = io_lib:format("Non-payable ~s `~s` cannot implement payable interface `~s`", + [KindStr, name(ContractCon), name(InterfaceCon)]), + mk_t_err(pos(ContractCon), Msg); mk_error(Err) -> Msg = io_lib:format("Unknown error: ~p", [Err]), mk_t_err(pos(0, 0), Msg). diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 0ca006b..81f2cef 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -205,6 +205,9 @@ compilable_contracts() -> "polymorphism_variance_switching_chain_create", "polymorphism_variance_switching_void_supertype", "polymorphism_variance_switching_unify_with_interface_decls", + "polymorphism_preserve_or_add_payable_contract", + "polymorphism_preserve_or_add_payable_entrypoint", + "polymorphism_preserve_or_remove_stateful_entrypoint", "missing_init_fun_state_unit", "complex_compare_leq", "complex_compare", @@ -868,7 +871,7 @@ failing_contracts() -> " - line 9, column 5">>]) , ?TYPE_ERROR(polymorphism_contract_missing_implementation, [<> + "Unimplemented entrypoint `f` from the interface `I1` in the contract `I2`">> ]) , ?TYPE_ERROR(polymorphism_contract_same_decl_multi_interface, [< "to arguments\n" " `Chain.create : (value : int, var_args) => 'c`">> ]) + , ?TYPE_ERROR(polymorphism_add_stateful_entrypoint, + [<> + ]) + , ?TYPE_ERROR(polymorphism_change_entrypoint_to_function, + [<> + ]) + , ?TYPE_ERROR(polymorphism_non_payable_contract_implement_payable, + [<> + ]) + , ?TYPE_ERROR(polymorphism_non_payable_interface_implement_payable, + [<> + ]) + , ?TYPE_ERROR(polymorphism_remove_payable_entrypoint, + [<> + ]) ]. validation_test_() -> diff --git a/test/contracts/polymorphism_add_stateful_entrypoint.aes b/test/contracts/polymorphism_add_stateful_entrypoint.aes new file mode 100644 index 0000000..26b279c --- /dev/null +++ b/test/contracts/polymorphism_add_stateful_entrypoint.aes @@ -0,0 +1,5 @@ +contract interface I = + entrypoint f : () => int + +contract C : I = + stateful entrypoint f() = 1 diff --git a/test/contracts/polymorphism_change_entrypoint_to_function.aes b/test/contracts/polymorphism_change_entrypoint_to_function.aes new file mode 100644 index 0000000..38c9614 --- /dev/null +++ b/test/contracts/polymorphism_change_entrypoint_to_function.aes @@ -0,0 +1,6 @@ +contract interface I = + entrypoint f : () => int + +contract C : I = + entrypoint init() = () + function f() = 1 diff --git a/test/contracts/polymorphism_non_payable_contract_implement_payable.aes b/test/contracts/polymorphism_non_payable_contract_implement_payable.aes new file mode 100644 index 0000000..59c0fca --- /dev/null +++ b/test/contracts/polymorphism_non_payable_contract_implement_payable.aes @@ -0,0 +1,5 @@ +payable contract interface I = + payable entrypoint f : () => int + +contract C : I = + entrypoint f() = 123 diff --git a/test/contracts/polymorphism_non_payable_interface_implement_payable.aes b/test/contracts/polymorphism_non_payable_interface_implement_payable.aes new file mode 100644 index 0000000..3849ea5 --- /dev/null +++ b/test/contracts/polymorphism_non_payable_interface_implement_payable.aes @@ -0,0 +1,8 @@ +payable contract interface I = + payable entrypoint f : () => int + +contract interface H : I = + payable entrypoint f : () => int + +payable contract C : H = + entrypoint f() = 123 diff --git a/test/contracts/polymorphism_preserve_or_add_payable_contract.aes b/test/contracts/polymorphism_preserve_or_add_payable_contract.aes new file mode 100644 index 0000000..1d8877c --- /dev/null +++ b/test/contracts/polymorphism_preserve_or_add_payable_contract.aes @@ -0,0 +1,14 @@ +contract interface F = + entrypoint f : () => int + +payable contract interface G : F = + payable entrypoint f : () => int + entrypoint g : () => int + +payable contract interface H = + payable entrypoint h : () => int + +payable contract C : G, H = + payable entrypoint f() = 1 + payable entrypoint g() = 2 + payable entrypoint h() = 3 diff --git a/test/contracts/polymorphism_preserve_or_add_payable_entrypoint.aes b/test/contracts/polymorphism_preserve_or_add_payable_entrypoint.aes new file mode 100644 index 0000000..9940a8b --- /dev/null +++ b/test/contracts/polymorphism_preserve_or_add_payable_entrypoint.aes @@ -0,0 +1,7 @@ +contract interface I = + payable entrypoint f : () => int + entrypoint g : () => int + +contract C : I = + payable entrypoint f() = 1 + payable entrypoint g() = 2 diff --git a/test/contracts/polymorphism_preserve_or_remove_stateful_entrypoint.aes b/test/contracts/polymorphism_preserve_or_remove_stateful_entrypoint.aes new file mode 100644 index 0000000..71a39dc --- /dev/null +++ b/test/contracts/polymorphism_preserve_or_remove_stateful_entrypoint.aes @@ -0,0 +1,7 @@ +contract interface I = + stateful entrypoint f : () => int + stateful entrypoint g : () => int + +contract C : I = + stateful entrypoint f() = 1 + entrypoint g() = 2 diff --git a/test/contracts/polymorphism_remove_payable_entrypoint.aes b/test/contracts/polymorphism_remove_payable_entrypoint.aes new file mode 100644 index 0000000..b916ca1 --- /dev/null +++ b/test/contracts/polymorphism_remove_payable_entrypoint.aes @@ -0,0 +1,5 @@ +contract interface I = + payable entrypoint f : () => int + +contract C : I = + entrypoint f() = 1