Add functions as fields before inferring

Unbound untyped fields before binding typed ones

Fix failing tests

Make complex_types contract non-compatible with aevm

Reduce code duplication

Undo changes to test.aes

Remove special handling of __constructor__ field

Resolve field constraint by arity of contract function

Update CHANGELOG

Update CHANGELOG.md

Co-authored-by: Radosław Rowicki <35342116+radrow@users.noreply.github.com>

Split bind_field function

Add a comment about rebinding
This commit is contained in:
Gaith Hallak 2022-04-06 19:52:48 +03:00
parent 34b52739fd
commit 99364222ea
7 changed files with 82 additions and 46 deletions

View File

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Options to enable/disable certain optimizations. - 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 ### Changed
### Removed ### Removed
### Fixed ### Fixed

View File

@ -270,15 +270,24 @@ bind_state(Env) ->
false -> Env1 false -> Env1
end. end.
-spec bind_field(name(), field_info(), env()) -> env(). -spec bind_field_append(name(), field_info(), env()) -> env().
bind_field(X, Info, Env = #env{ fields = Fields }) -> bind_field_append(X, Info, Env = #env{ fields = Fields }) ->
Fields1 = maps:update_with(X, fun(Infos) -> [Info | Infos] end, [Info], Fields), Fields1 = maps:update_with(X, fun(Infos) -> [Info | Infos] end, [Info], Fields),
Env#env{ fields = Fields1 }. Env#env{ fields = Fields1 }.
-spec bind_fields([{name(), field_info()}], env()) -> env(). -spec bind_field_update(name(), field_info(), env()) -> env().
bind_fields([], Env) -> Env; bind_field_update(X, Info, Env = #env{ fields = Fields }) ->
bind_fields([{Id, Info} | Rest], Env) -> Fields1 = maps:update_with(X, fun([_ | Infos]) -> [Info | Infos]; ([]) -> [Info] end, [Info], Fields),
bind_fields(Rest, bind_field(Id, Info, Env)). 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 %% Contract entrypoints take three named arguments
%% gas : int = Call.gas_left() %% 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")))], Named("protected", Typed({bool, Ann, false}, Id("bool")))],
Args, {if_t, Ann, Id("protected"), {app_t, Ann, {id, Ann, "option"}, [Ret]}, Ret}}. Args, {if_t, Ann, Id("protected"), {app_t, Ann, {id, Ann, "option"}, [Ret]}, Ret}}.
-spec bind_contract(aeso_syntax:decl(), env()) -> env(). -spec bind_contract(typed | untyped, aeso_syntax:decl(), env()) -> env().
bind_contract({Contract, Ann, Id, _Impls, Contents}, Env) bind_contract(Typing, {Contract, Ann, Id, _Impls, Contents}, Env)
when ?IS_CONTRACT_HEAD(Contract) -> when ?IS_CONTRACT_HEAD(Contract) ->
Key = name(Id), Key = name(Id),
Sys = [{origin, system}], Sys = [{origin, system}],
Fields = TypeOrFresh = fun({typed, _, _, Type}) -> Type; (_) -> fresh_uvar(Sys) end,
Fields =
[ {field_t, AnnF, Entrypoint, contract_call_type(Type)} [ {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, [ {field_t, AnnF, Entrypoint,
contract_call_type( 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" Name =/= "init"
] ++ ] ++
%% Predefined fields %% Predefined fields
[ {field_t, Sys, {id, Sys, "address"}, {id, Sys, "address"}} ] ++ [ {field_t, Sys, {id, Sys, "address"}, {id, Sys, "address"}} ] ++
[ {field_t, Sys, {id, Sys, ?CONSTRUCTOR_MOCK_NAME}, [ {field_t, Sys, {id, Sys, ?CONSTRUCTOR_MOCK_NAME},
contract_call_type( contract_call_type(
case [ [ArgT || {typed, _, _, ArgT} <- Args] case [ [TypeOrFresh(Arg) || Arg <- Args]
|| {letfun, AnnF, {id, _, "init"}, Args, _, _} <- Contents, || {letfun, AnnF, {id, _, "init"}, Args, _, _} <- Contents,
aeso_syntax:get_ann(entrypoint, AnnF, false)] aeso_syntax:get_ann(entrypoint, AnnF, false)]
++ [ Args ++ [ Args
@ -337,7 +347,7 @@ bind_contract({Contract, Ann, Id, _Impls, Contents}, Env)
record_t = Id }} record_t = Id }}
|| {field_t, _, {id, FieldAnn, Entrypoint}, Type} <- Fields ], || {field_t, _, {id, FieldAnn, Entrypoint}, Type} <- Fields ],
bind_type(Key, Ann, {[], {contract_t, 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? %% What scopes could a given name come from?
-spec possible_scopes(env(), qname()) -> [qname()]. -spec possible_scopes(env(), qname()) -> [qname()].
@ -447,6 +457,9 @@ lookup_env1(#env{ namespace = Current, used_namespaces = UsedNamespaces, scopes
end end
end. end.
fun_arity({fun_t, _, _, Args, _}) -> length(Args);
fun_arity(_) -> none.
-spec lookup_record_field(env(), name()) -> [field_info()]. -spec lookup_record_field(env(), name()) -> [field_info()].
lookup_record_field(Env, FieldName) -> lookup_record_field(Env, FieldName) ->
maps:get(FieldName, Env#env.fields, []). 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), [ Fld || Fld = #field_info{ kind = K } <- lookup_record_field(Env, FieldName),
Kind == project orelse K /= contract ]. 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 ------------------------------------------------------ %% -- Name manipulation ------------------------------------------------------
-spec qname(type_id()) -> qname(). -spec qname(type_id()) -> qname().
@ -872,7 +890,7 @@ infer(Contracts, Options) ->
-spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()], list(option())) -> -spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()], list(option())) ->
{env(), [aeso_syntax:decl()]}. {env(), [aeso_syntax:decl()]}.
infer1(Env, [], Acc, _Options) -> {Env, lists:reverse(Acc)}; 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) -> when ?IS_CONTRACT_HEAD(Contract) ->
%% do type inference on each contract independently. %% do type inference on each contract independently.
Env = Env0#env{ contract_parents = maps:put(name(ConName), 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 contract_interface -> ok
end, end,
populate_functions_to_implement(Env, ConName, Impls, Acc), populate_functions_to_implement(Env, ConName, Impls, Acc),
{Env1, Code1} = infer_contract_top(push_scope(contract, ConName, Env), What, Code, Options), Env1 = bind_contract(untyped, Contract0, Env),
report_unimplemented_functions(Env, ConName), {Env2, Code1} = infer_contract_top(push_scope(contract, ConName, Env1), What, Code, Options),
report_unimplemented_functions(Env1, ConName),
Contract1 = {Contract, Ann, ConName, Impls, Code1}, Contract1 = {Contract, Ann, ConName, Impls, Code1},
Env2 = pop_scope(Env1), Env3 = pop_scope(Env2),
Env3 = bind_contract(Contract1, Env2), %% Rebinding because the qualifications of types are added during type inference. Could we do better?
infer1(Env3, Rest, [Contract1 | Acc], Options); Env4 = bind_contract(typed, Contract1, Env3),
infer1(Env4, Rest, [Contract1 | Acc], Options);
infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) -> infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) ->
when_warning(warn_unused_includes, when_warning(warn_unused_includes,
fun() -> 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(). -spec check_fields(env(), #{ name() => aeso_syntax:decl() }, type(), [aeso_syntax:field_t()]) -> env().
check_fields(Env, _TypeMap, _, []) -> Env; check_fields(Env, _TypeMap, _, []) -> Env;
check_fields(Env, TypeMap, RecTy, [{field_t, Ann, Id, Type} | Fields]) -> 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_fields(Env1, TypeMap, RecTy, Fields).
check_parameterizable({id, Ann, "event"}, [_ | _]) -> check_parameterizable({id, Ann, "event"}, [_ | _]) ->
@ -2336,7 +2356,12 @@ solve_constraints(Env) ->
field_t = FieldType, field_t = FieldType,
kind = Kind, kind = Kind,
context = When }) -> 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}), type_error({undefined_field, Field}),
false; false;

View File

@ -217,6 +217,7 @@ compilable_contracts() ->
"polymorphic_map_keys", "polymorphic_map_keys",
"unapplied_contract_call", "unapplied_contract_call",
"unapplied_named_arg_builtin", "unapplied_named_arg_builtin",
"resolve_field_constraint_by_arity",
"test" % Custom general-purpose test file. Keep it last on the list. "test" % Custom general-purpose test file. Keep it last on the list.
]. ].
@ -733,10 +734,22 @@ failing_contracts() ->
"Conflicting updates for field 'foo'">>]) "Conflicting updates for field 'foo'">>])
, ?TYPE_ERROR(factories_type_errors, , ?TYPE_ERROR(factories_type_errors,
[<<?Pos(10,18) [<<?Pos(10,18)
"Chain.clone requires `ref` named argument of contract type.">>, "Chain.clone requires `ref` named argument of contract type.">>,
<<?Pos(11,18) <<?Pos(11,18)
"Cannot unify `(gas : int, value : int, protected : bool) => if(protected, option(void), void)` and `(gas : int, value : int, protected : bool, int, bool) => 'b`\n" "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)\nagainst the expected type\n (gas : int, value : int, protected : bool, int, bool) => 'b">>, "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)">>,
<<?Pos(11,18)
"Cannot unify `Bakoom` and `Kaboom`\n"
"when checking that contract construction of type\n"
" Bakoom\n"
"arising from resolution of variadic function `Chain.clone`\n"
"matches the expected type\n"
" Kaboom">>,
<<?Pos(12,37) <<?Pos(12,37)
"Cannot unify `int` and `bool`\n" "Cannot unify `int` and `bool`\n"
"when checking named argument `gas : int` against inferred type `bool`">>, "when checking named argument `gas : int` against inferred type `bool`">>,

View File

@ -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 = contract ComplexTypes =
record state = { worker : Remote } record state = { worker : ComplexTypes }
entrypoint init(worker) = {worker = worker} entrypoint init(worker) = {worker = worker}

View File

@ -1,14 +1,9 @@
// Testing primitives for accessing the block chain environment // 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 = contract Environment =
record state = {remote : Interface} record state = {remote : Environment}
entrypoint init(remote) = {remote = remote} entrypoint init(remote) = {remote = remote}

View File

@ -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()

View File

@ -1,7 +1,4 @@
contract interface SpendContract =
entrypoint withdraw : (int) => int
contract SpendTest = contract SpendTest =
stateful entrypoint spend(to, amount) = stateful entrypoint spend(to, amount) =