diff --git a/CHANGELOG.md b/CHANGELOG.md index 32cadfe..ab4867a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Options to enable/disable certain optimizations. +- The ability to call a different instance of the current contract + ``` + contract Main = + entrypoint spend(x : int) : int = x + entrypoint f(c : Main) : int = c.spend(10) + ``` ### Changed ### Removed ### Fixed diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index b1fdffb..03cbe2f 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -270,15 +270,24 @@ bind_state(Env) -> false -> Env1 end. --spec bind_field(name(), field_info(), env()) -> env(). -bind_field(X, Info, Env = #env{ fields = Fields }) -> +-spec bind_field_append(name(), field_info(), env()) -> env(). +bind_field_append(X, Info, Env = #env{ fields = Fields }) -> Fields1 = maps:update_with(X, fun(Infos) -> [Info | Infos] end, [Info], Fields), Env#env{ fields = Fields1 }. --spec bind_fields([{name(), field_info()}], env()) -> env(). -bind_fields([], Env) -> Env; -bind_fields([{Id, Info} | Rest], Env) -> - bind_fields(Rest, bind_field(Id, Info, Env)). +-spec bind_field_update(name(), field_info(), env()) -> env(). +bind_field_update(X, Info, Env = #env{ fields = Fields }) -> + Fields1 = maps:update_with(X, fun([_ | Infos]) -> [Info | Infos]; ([]) -> [Info] end, [Info], Fields), + Env#env{ fields = Fields1 }. + +-spec bind_fields([{name(), field_info()}], typed | untyped, env()) -> env(). +bind_fields([], _Typing, Env) -> Env; +bind_fields([{Id, Info} | Rest], Typing, Env) -> + NewEnv = case Typing of + untyped -> bind_field_append(Id, Info, Env); + typed -> bind_field_update(Id, Info, Env) + end, + bind_fields(Rest, Typing, NewEnv). %% Contract entrypoints take three named arguments %% gas : int = Call.gas_left() @@ -296,26 +305,27 @@ contract_call_type({fun_t, Ann, [], Args, Ret}) -> Named("protected", Typed({bool, Ann, false}, Id("bool")))], Args, {if_t, Ann, Id("protected"), {app_t, Ann, {id, Ann, "option"}, [Ret]}, Ret}}. --spec bind_contract(aeso_syntax:decl(), env()) -> env(). -bind_contract({Contract, Ann, Id, _Impls, Contents}, Env) +-spec bind_contract(typed | untyped, aeso_syntax:decl(), env()) -> env(). +bind_contract(Typing, {Contract, Ann, Id, _Impls, Contents}, Env) when ?IS_CONTRACT_HEAD(Contract) -> - Key = name(Id), - Sys = [{origin, system}], - Fields = + Key = name(Id), + Sys = [{origin, system}], + TypeOrFresh = fun({typed, _, _, Type}) -> Type; (_) -> fresh_uvar(Sys) end, + Fields = [ {field_t, AnnF, Entrypoint, contract_call_type(Type)} - || {fun_decl, AnnF, Entrypoint, Type} <- Contents ] ++ + || {fun_decl, AnnF, Entrypoint, Type = {fun_t, _, _, _, _}} <- Contents ] ++ [ {field_t, AnnF, Entrypoint, contract_call_type( - {fun_t, AnnF, [], [ArgT || {typed, _, _, ArgT} <- Args], RetT}) + {fun_t, AnnF, [], [TypeOrFresh(Arg) || Arg <- Args], TypeOrFresh(Ret)}) } - || {letfun, AnnF, Entrypoint = {id, _, Name}, Args, _Type, [{guarded, _, [], {typed, _, _, RetT}}]} <- Contents, + || {letfun, AnnF, Entrypoint = {id, _, Name}, Args, _Type, [{guarded, _, [], Ret}]} <- Contents, Name =/= "init" ] ++ %% Predefined fields [ {field_t, Sys, {id, Sys, "address"}, {id, Sys, "address"}} ] ++ [ {field_t, Sys, {id, Sys, ?CONSTRUCTOR_MOCK_NAME}, contract_call_type( - case [ [ArgT || {typed, _, _, ArgT} <- Args] + case [ [TypeOrFresh(Arg) || Arg <- Args] || {letfun, AnnF, {id, _, "init"}, Args, _, _} <- Contents, aeso_syntax:get_ann(entrypoint, AnnF, false)] ++ [ Args @@ -337,7 +347,7 @@ bind_contract({Contract, Ann, Id, _Impls, Contents}, Env) record_t = Id }} || {field_t, _, {id, FieldAnn, Entrypoint}, Type} <- Fields ], bind_type(Key, Ann, {[], {contract_t, Fields}}, - bind_fields(FieldInfo, Env)). + bind_fields(FieldInfo, Typing, Env)). %% What scopes could a given name come from? -spec possible_scopes(env(), qname()) -> [qname()]. @@ -447,6 +457,9 @@ lookup_env1(#env{ namespace = Current, used_namespaces = UsedNamespaces, scopes end end. +fun_arity({fun_t, _, _, Args, _}) -> length(Args); +fun_arity(_) -> none. + -spec lookup_record_field(env(), name()) -> [field_info()]. lookup_record_field(Env, FieldName) -> maps:get(FieldName, Env#env.fields, []). @@ -457,6 +470,11 @@ lookup_record_field(Env, FieldName, Kind) -> [ Fld || Fld = #field_info{ kind = K } <- lookup_record_field(Env, FieldName), Kind == project orelse K /= contract ]. +lookup_record_field_arity(Env, FieldName, Arity, Kind) -> + Fields = lookup_record_field(Env, FieldName, Kind), + [ Fld || Fld = #field_info{ field_t = FldType } <- Fields, + fun_arity(dereference_deep(FldType)) == Arity ]. + %% -- Name manipulation ------------------------------------------------------ -spec qname(type_id()) -> qname(). @@ -872,7 +890,7 @@ infer(Contracts, Options) -> -spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()], list(option())) -> {env(), [aeso_syntax:decl()]}. infer1(Env, [], Acc, _Options) -> {Env, lists:reverse(Acc)}; -infer1(Env0, [{Contract, Ann, ConName, Impls, Code} | Rest], Acc, Options) +infer1(Env0, [Contract0 = {Contract, Ann, ConName, Impls, Code} | Rest], Acc, Options) when ?IS_CONTRACT_HEAD(Contract) -> %% do type inference on each contract independently. Env = Env0#env{ contract_parents = maps:put(name(ConName), @@ -889,12 +907,14 @@ infer1(Env0, [{Contract, Ann, ConName, Impls, Code} | Rest], Acc, Options) contract_interface -> ok end, populate_functions_to_implement(Env, ConName, Impls, Acc), - {Env1, Code1} = infer_contract_top(push_scope(contract, ConName, Env), What, Code, Options), - report_unimplemented_functions(Env, ConName), + Env1 = bind_contract(untyped, Contract0, Env), + {Env2, Code1} = infer_contract_top(push_scope(contract, ConName, Env1), What, Code, Options), + report_unimplemented_functions(Env1, ConName), Contract1 = {Contract, Ann, ConName, Impls, Code1}, - Env2 = pop_scope(Env1), - Env3 = bind_contract(Contract1, Env2), - infer1(Env3, Rest, [Contract1 | Acc], Options); + Env3 = pop_scope(Env2), + %% Rebinding because the qualifications of types are added during type inference. Could we do better? + Env4 = bind_contract(typed, Contract1, Env3), + infer1(Env4, Rest, [Contract1 | Acc], Options); infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) -> when_warning(warn_unused_includes, fun() -> @@ -1367,7 +1387,7 @@ check_named_arg(Env, {named_arg_t, Ann, Id, Type, Default}) -> -spec check_fields(env(), #{ name() => aeso_syntax:decl() }, type(), [aeso_syntax:field_t()]) -> env(). check_fields(Env, _TypeMap, _, []) -> Env; check_fields(Env, TypeMap, RecTy, [{field_t, Ann, Id, Type} | Fields]) -> - Env1 = bind_field(name(Id), #field_info{ ann = Ann, kind = record, field_t = Type, record_t = RecTy }, Env), + Env1 = bind_field_append(name(Id), #field_info{ ann = Ann, kind = record, field_t = Type, record_t = RecTy }, Env), check_fields(Env1, TypeMap, RecTy, Fields). check_parameterizable({id, Ann, "event"}, [_ | _]) -> @@ -2336,7 +2356,12 @@ solve_constraints(Env) -> field_t = FieldType, kind = Kind, context = When }) -> - case lookup_record_field(Env, FieldName, Kind) of + Arity = fun_arity(dereference_deep(FieldType)), + FieldInfos = case Arity of + none -> lookup_record_field(Env, FieldName, Kind); + _ -> lookup_record_field_arity(Env, FieldName, Arity, Kind) + end, + case FieldInfos of [] -> type_error({undefined_field, Field}), false; diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index fc65752..3652aad 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -217,6 +217,7 @@ compilable_contracts() -> "polymorphic_map_keys", "unapplied_contract_call", "unapplied_named_arg_builtin", + "resolve_field_constraint_by_arity", "test" % Custom general-purpose test file. Keep it last on the list. ]. @@ -733,10 +734,22 @@ failing_contracts() -> "Conflicting updates for field 'foo'">>]) , ?TYPE_ERROR(factories_type_errors, [<>, + "Chain.clone requires `ref` named argument of contract type.">>, < if(protected, option(void), void)` 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">>, + "Cannot unify `(gas : int, value : int, protected : bool) => if(protected, option(void), void)` and `(gas : int, value : int, protected : bool, int, bool) => if(protected, option(void), void)`\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)\n" + "against the expected type\n" + " (gas : int, value : int, protected : bool, int, bool) =>\n" + " if(protected, option(void), void)">>, + <>, <>, diff --git a/test/contracts/complex_types.aes b/test/contracts/complex_types.aes index 83b9b69..df7dc46 100644 --- a/test/contracts/complex_types.aes +++ b/test/contracts/complex_types.aes @@ -1,16 +1,7 @@ -contract interface Remote = - entrypoint up_to : (int) => list(int) - entrypoint sum : (list(int)) => int - entrypoint some_string : () => string - entrypoint pair : (int, string) => int * string - entrypoint squares : (int) => list(int * int) - entrypoint filter_some : (list(option(int))) => list(int) - entrypoint all_some : (list(option(int))) => option(list(int)) - contract ComplexTypes = - record state = { worker : Remote } + record state = { worker : ComplexTypes } entrypoint init(worker) = {worker = worker} diff --git a/test/contracts/environment.aes b/test/contracts/environment.aes index 7c02e6b..cf21a36 100644 --- a/test/contracts/environment.aes +++ b/test/contracts/environment.aes @@ -1,14 +1,9 @@ // Testing primitives for accessing the block chain environment -contract interface Interface = - entrypoint contract_address : () => address - entrypoint call_origin : () => address - entrypoint call_caller : () => address - entrypoint call_value : () => int contract Environment = - record state = {remote : Interface} + record state = {remote : Environment} entrypoint init(remote) = {remote = remote} diff --git a/test/contracts/resolve_field_constraint_by_arity.aes b/test/contracts/resolve_field_constraint_by_arity.aes new file mode 100644 index 0000000..ee8d1f9 --- /dev/null +++ b/test/contracts/resolve_field_constraint_by_arity.aes @@ -0,0 +1,9 @@ +contract First = + entrypoint print_num(x) = 1 + x + +contract Second = + entrypoint print_num() = 1 + +main contract Test = + entrypoint f(c) = c.print_num(1) + entrypoint g(c) = c.print_num() diff --git a/test/contracts/spend_test.aes b/test/contracts/spend_test.aes index 0eb1666..2bd61fd 100644 --- a/test/contracts/spend_test.aes +++ b/test/contracts/spend_test.aes @@ -1,7 +1,4 @@ -contract interface SpendContract = - entrypoint withdraw : (int) => int - contract SpendTest = stateful entrypoint spend(to, amount) =