Merge pull request #125 from aeternity/PT-162578406-payable_modifier

PT-162578406 Add payable modifier
This commit is contained in:
Hans Svensson 2019-08-19 16:27:18 +02:00 committed by GitHub
commit 73b9a54172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 94 additions and 45 deletions

View File

@ -2,7 +2,7 @@
{erl_opts, [debug_info]}.
{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"fdd660a"}}}
{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"befa1e3"}}}
, {getopt, "1.0.1"}
, {eblake2, "1.0.0"}
, {jsx, {git, "https://github.com/talentdeficit/jsx.git",

View File

@ -1,7 +1,7 @@
{"1.1.0",
[{<<"aebytecode">>,
{git,"https://github.com/aeternity/aebytecode.git",
{ref,"fdd660a2191f19e1bd77cafe0d39c826539c1b6c"}},
{ref,"befa1e3ff9c53c1054917c82e3b30c1c3c743f4f"}},
0},
{<<"aeserialization">>,
{git,"https://github.com/aeternity/aeserialization.git",

View File

@ -113,7 +113,7 @@ encode_contract(Contract = {contract, _, {con, _, Name}, _}) ->
|| F <- sort_decls(contract_funcs(Contract)),
is_entrypoint(F) ],
#{contract => C3#{functions => Fdefs}};
#{contract => C3#{functions => Fdefs, payable => is_payable(Contract)}};
encode_contract(Namespace = {namespace, _, {con, _, Name}, _}) ->
Tdefs = [ encode_typedef(T) || T <- sort_decls(contract_types(Namespace)) ],
#{namespace => #{name => encode_name(Name),
@ -125,12 +125,14 @@ encode_function(FDef = {letfun, _, {id, _, Name}, Args, Type, _}) ->
#{name => encode_name(Name),
arguments => encode_args(Args),
returns => encode_type(Type),
stateful => is_stateful(FDef)};
stateful => is_stateful(FDef),
payable => is_payable(FDef)};
encode_function(FDecl = {fun_decl, _, {id, _, Name}, {fun_t, _, _, Args, Type}}) ->
#{name => encode_name(Name),
arguments => encode_anon_args(Args),
returns => encode_type(Type),
stateful => is_stateful(FDecl)}.
stateful => is_stateful(FDecl),
payable => is_payable(FDecl)}.
encode_anon_args(Types) ->
Anons = [ list_to_binary("_" ++ integer_to_list(X)) || X <- lists:seq(1, length(Types))],
@ -234,12 +236,13 @@ do_render_aci_json(Json) ->
{ok, list_to_binary(string:join(DecodedContracts, "\n"))}.
decode_contract(#{contract := #{name := Name,
payable := Payable,
type_defs := Ts0,
functions := Fs} = C}) ->
MkTDef = fun(N, T) -> #{name => N, vars => [], typedef => T} end,
Ts = [ MkTDef(<<"state">>, maps:get(state, C)) || maps:is_key(state, C) ] ++
[ MkTDef(<<"event">>, maps:get(event, C)) || maps:is_key(event, C) ] ++ Ts0,
["contract ", io_lib:format("~s", [Name])," =\n",
[payable(Payable), "contract ", io_lib:format("~s", [Name])," =\n",
decode_tdefs(Ts), decode_funcs(Fs)];
decode_contract(#{namespace := #{name := Name, type_defs := Ts}}) when Ts /= [] ->
["namespace ", io_lib:format("~s", [Name])," =\n",
@ -249,8 +252,8 @@ decode_contract(_) -> [].
decode_funcs(Fs) -> [ decode_func(F) || F <- Fs ].
%% decode_func(#{name := init}) -> [];
decode_func(#{name := Name, arguments := As, returns := T}) ->
[" entrypoint", " ", io_lib:format("~s", [Name]), " : ",
decode_func(#{name := Name, payable := Payable, arguments := As, returns := T}) ->
[" ", payable(Payable), "entrypoint ", io_lib:format("~s", [Name]), " : ",
decode_args(As), " => ", decode_type(T), $\n].
decode_args(As) ->
@ -321,13 +324,16 @@ decode_deftype(#{record := _Efs}) -> "record";
decode_deftype(#{variant := _}) -> "datatype";
decode_deftype(_T) -> "type".
decode_tvars([]) -> []; %No tvars, no parentheses
decode_tvars([]) -> []; %No tvars, no parentheses
decode_tvars(Vs) ->
Dvs = [ decode_tvar(V) || V <- Vs ],
[$(,lists:join(", ", Dvs),$)].
decode_tvar(#{name := N}) -> io_lib:format("~s", [N]).
payable(true) -> "payable ";
payable(false) -> "".
%% #contract{Ann, Con, [Declarations]}.
contract_funcs({C, _, _, Decls}) when C == contract; C == namespace ->
@ -352,6 +358,7 @@ sort_decls(Ds) ->
is_entrypoint(Node) -> aeso_syntax:get_ann(entrypoint, Node, false).
is_stateful(Node) -> aeso_syntax:get_ann(stateful, Node, false).
is_payable(Node) -> aeso_syntax:get_ann(payable, Node, false).
typedef_name({type_def, _, {id, _, Name}, _, _}) -> Name.

View File

@ -504,7 +504,8 @@ global_env() ->
IntScope = #scope{ funs = MkDefs([{"to_str", Fun1(Int, String)}]) },
AddressScope = #scope{ funs = MkDefs([{"to_str", Fun1(Address, String)},
{"is_oracle", Fun1(Address, Bool)},
{"is_contract", Fun1(Address, Bool)}]) },
{"is_contract", Fun1(Address, Bool)},
{"is_payable", Fun1(Address, Bool)}]) },
#env{ scopes =
#{ [] => TopScope

View File

@ -16,7 +16,7 @@
-type option() :: term().
-type attribute() :: stateful | pure | private.
-type attribute() :: stateful | payable | pure | private.
-type fun_name() :: {entrypoint, binary()}
| {local_fun, [string()]}
@ -109,7 +109,8 @@
-type fcode() :: #{ contract_name := string(),
state_type := ftype(),
event_type := ftype() | none,
functions := #{ fun_name() => fun_def() } }.
functions := #{ fun_name() => fun_def() },
payable := boolean() }.
-type type_def() :: fun(([ftype()]) -> ftype()).
@ -196,7 +197,7 @@ builtins() ->
{"union", 2}, {"difference", 2}, {"none", none}, {"all", none}]},
{["Bytes"], [{"to_int", 1}, {"to_str", 1}]},
{["Int"], [{"to_str", 1}]},
{["Address"], [{"to_str", 1}, {"is_oracle", 1}, {"is_contract", 1}]}
{["Address"], [{"to_str", 1}, {"is_oracle", 1}, {"is_contract", 1}, {"is_payable", 1}]}
],
maps:from_list([ {NS ++ [Fun], {MkName(NS, Fun), Arity}}
|| {NS, Funs} <- Scopes,
@ -227,7 +228,7 @@ init_type_env() ->
%% -- Compilation ------------------------------------------------------------
-spec to_fcode(env(), aeso_syntax:ast()) -> fcode().
to_fcode(Env, [{contract, _, {con, _, Main}, Decls}]) ->
to_fcode(Env, [{contract, Attrs, {con, _, Main}, Decls}]) ->
#{ builtins := Builtins } = Env,
MainEnv = Env#{ context => {main_contract, Main},
builtins => Builtins#{[Main, "state"] => {get_state, none},
@ -237,9 +238,11 @@ to_fcode(Env, [{contract, _, {con, _, Main}, Decls}]) ->
decls_to_fcode(MainEnv, Decls),
StateType = lookup_type(Env1, [Main, "state"], [], {tuple, []}),
EventType = lookup_type(Env1, [Main, "event"], [], none),
Payable = proplists:get_value(payable, Attrs, false),
#{ contract_name => Main,
state_type => StateType,
event_type => EventType,
payable => Payable,
functions => add_init_function(Env1,
add_event_function(Env1, EventType, Funs)) };
to_fcode(Env, [{contract, _, {con, _, Con}, Decls} | Code]) ->
@ -1304,7 +1307,9 @@ field_value({field_t, _, {id, _, X}, _}, Fields) ->
%% -- Attributes --
get_attributes(Ann) ->
[stateful || proplists:get_value(stateful, Ann, false)].
[stateful || proplists:get_value(stateful, Ann, false)] ++
[payable || proplists:get_value(payable, Ann, false)] ++
[private || not proplists:get_value(entrypoint, Ann, false)].
%% -- Basic utilities --

View File

@ -17,11 +17,15 @@
-spec convert_typed(aeso_syntax:ast(), list()) -> aeso_icode:icode().
convert_typed(TypedTree, Options) ->
Name = case lists:last(TypedTree) of
{contract, _, {con, _, Con}, _} -> Con;
_ -> gen_error(last_declaration_must_be_contract)
{Payable, Name} =
case lists:last(TypedTree) of
{contract, Attrs, {con, _, Con}, _} ->
{proplists:get_value(payable, Attrs, false), Con};
_ ->
gen_error(last_declaration_must_be_contract)
end,
NewIcode = aeso_icode:set_name(Name, aeso_icode:new(Options)),
NewIcode = aeso_icode:set_payable(Payable,
aeso_icode:set_name(Name, aeso_icode:new(Options))),
Icode = code(TypedTree, NewIcode, Options),
deadcode_elimination(Icode).
@ -87,6 +91,7 @@ contract_to_icode([{type_def, _Attrib, Id = {id, _, Name}, Args, Def} | Rest],
contract_to_icode(Rest, Icode2);
contract_to_icode([{letfun, Attrib, Name, Args, _What, Body={typed,_,_,T}}|Rest], Icode) ->
FunAttrs = [ stateful || proplists:get_value(stateful, Attrib, false) ] ++
[ payable || proplists:get_value(payable, Attrib, false) ] ++
[ private || is_private(Attrib, Icode) ],
%% TODO: Handle types
FunName = ast_id(Name),
@ -428,6 +433,9 @@ ast_body(?qid_app(["Address", "is_oracle"], [Addr], _, _), Icode) ->
ast_body(?qid_app(["Address", "is_contract"], [Addr], _, _), Icode) ->
prim_call(?PRIM_CALL_ADDR_IS_CONTRACT, #integer{value = 0},
[ast_body(Addr, Icode)], [word], word);
ast_body(?qid_app(["Address", "is_payable"], [Addr], _, _), Icode) ->
prim_call(?PRIM_CALL_ADDR_IS_PAYABLE, #integer{value = 0},
[ast_body(Addr, Icode)], [word], word);
ast_body(?qid_app(["Bytes", "to_int"], [Bytes], _, _), Icode) ->
{typed, _, _, {bytes_t, _, N}} = Bytes,

View File

@ -123,7 +123,8 @@ from_string1(aevm, ContractString, Options) ->
compiler_version => Version,
contract_source => ContractString,
type_info => TypeInfo,
abi_version => aeb_aevm_abi:abi_version()
abi_version => aeb_aevm_abi:abi_version(),
payable => maps:get(payable, Icode)
}};
from_string1(fate, ContractString, Options) ->
#{fcode := FCode} = string_to_code(ContractString, Options),
@ -135,7 +136,8 @@ from_string1(fate, ContractString, Options) ->
contract_source => ContractString,
type_info => [],
fate_code => FateCode,
abi_version => aeb_fate_abi:abi_version()
abi_version => aeb_fate_abi:abi_version(),
payable => maps:get(payable, FCode)
}}.
-spec string_to_code(string(), options()) -> map().
@ -549,8 +551,9 @@ to_bytecode([], _) -> [].
extract_type_info(#{functions := Functions} =_Icode) ->
ArgTypesOnly = fun(As) -> [ T || {_, T} <- As ] end,
Payable = fun(Attrs) -> proplists:get_value(payable, Attrs, false) end,
TypeInfo = [aeb_aevm_abi:function_type_info(list_to_binary(lists:last(Name)),
ArgTypesOnly(Args), TypeRep)
Payable(Attrs), ArgTypesOnly(Args), TypeRep)
|| {Name, Attrs, Args,_Body, TypeRep} <- Functions,
not is_tuple(Name),
not lists:member(private, Attrs)

View File

@ -109,6 +109,7 @@
Op =:= 'ORACLE_CHECK_QUERY' orelse
Op =:= 'IS_ORACLE' orelse
Op =:= 'IS_CONTRACT' orelse
Op =:= 'IS_PAYABLE' orelse
Op =:= 'CREATOR' orelse
false)).
@ -148,15 +149,17 @@ make_function_name({local_fun, Xs}) -> list_to_binary("." ++ string:join(Xs,
functions_to_scode(ContractName, Functions, Options) ->
FunNames = maps:keys(Functions),
maps:from_list(
[ {make_function_name(Name), function_to_scode(ContractName, FunNames, Name, Args, Body, Type, Options)}
[ {make_function_name(Name), function_to_scode(ContractName, FunNames, Name, Attrs, Args, Body, Type, Options)}
|| {Name, #{args := Args,
body := Body,
attrs := Attrs,
return := Type}} <- maps:to_list(Functions)]).
function_to_scode(ContractName, Functions, _Name, Args, Body, ResType, _Options) ->
function_to_scode(ContractName, Functions, _Name, Attrs0, Args, Body, ResType, _Options) ->
{ArgTypes, ResType1} = typesig_to_scode(Args, ResType),
SCode = to_scode(init_env(ContractName, Functions, Args), Body),
{{ArgTypes, ResType1}, SCode}.
Attrs = Attrs0 -- [stateful], %% Only track private and payable from here.
SCode = to_scode(init_env(ContractName, Functions, Args), Body),
{Attrs, {ArgTypes, ResType1}, SCode}.
-define(tvars, '$tvars').
@ -204,7 +207,7 @@ add_default_init_function(SFuns, {tuple, []}) ->
error ->
Sig = {[], {tuple, []}},
Body = [tuple(0)],
SFuns#{ InitName => {Sig, Body} }
SFuns#{ InitName => {[], Sig, Body} }
end.
%% -- Phase I ----------------------------------------------------------------
@ -541,6 +544,8 @@ builtin_to_scode(Env, address_is_oracle, [_] = Args) ->
call_to_scode(Env, aeb_fate_ops:is_oracle(?a, ?a), Args);
builtin_to_scode(Env, address_is_contract, [_] = Args) ->
call_to_scode(Env, aeb_fate_ops:is_contract(?a, ?a), Args);
builtin_to_scode(Env, address_is_payable, [_] = Args) ->
call_to_scode(Env, aeb_fate_ops:is_payable(?a, ?a), Args);
builtin_to_scode(Env, aens_resolve, [_Name, _Key, _Type] = Args) ->
call_to_scode(Env, aeb_fate_ops:aens_resolve(?a, ?a, ?a, ?a), Args);
builtin_to_scode(Env, aens_preclaim, [_Sign, _Account, _Hash] = Args) ->
@ -628,12 +633,12 @@ flatten_s(I) -> I.
-define(MAX_SIMPL_ITERATIONS, 10).
optimize_fun(_Funs, Name, {{Args, Res}, Code}, Options) ->
optimize_fun(_Funs, Name, {Attrs, Sig, Code}, Options) ->
Code0 = flatten(Code),
debug(opt, Options, "Optimizing ~s\n", [Name]),
Code1 = simpl_loop(0, Code0, Options),
Code2 = desugar(Code1),
{{Args, Res}, Code2}.
{Attrs, Sig, Code2}.
simpl_loop(N, Code, Options) when N >= ?MAX_SIMPL_ITERATIONS ->
debug(opt, Options, " No simpl_loop fixed_point after ~p iterations.\n\n", [N]),
@ -834,6 +839,7 @@ attributes(I) ->
{'ORACLE_CHECK_QUERY', A, B, C, D, E} -> Impure(A, [B, C, D, E]);
{'IS_ORACLE', A, B} -> Impure(A, [B]);
{'IS_CONTRACT', A, B} -> Impure(A, [B]);
{'IS_PAYABLE', A, B} -> Impure(A, [B]);
{'CREATOR', A} -> Pure(A, []);
{'ADDRESS', A} -> Pure(A, []);
{'BALANCE', A} -> Impure(A, []);
@ -1282,9 +1288,9 @@ desugar(I) -> [I].
to_basic_blocks(Funs) ->
to_basic_blocks(maps:to_list(Funs), aeb_fate_code:new()).
to_basic_blocks([{Name, {Sig, Code}}|Left], Acc) ->
to_basic_blocks([{Name, {Attrs, Sig, Code}}|Left], Acc) ->
BB = bb(Name, Code ++ [aeb_fate_ops:return()]),
to_basic_blocks(Left, aeb_fate_code:insert_fun(Name, Sig, BB, Acc));
to_basic_blocks(Left, aeb_fate_code:insert_fun(Name, Attrs, Sig, BB, Acc));
to_basic_blocks([], Acc) ->
Acc.

View File

@ -13,6 +13,7 @@
pp/1,
set_name/2,
set_namespace/2,
set_payable/2,
enter_namespace/2,
get_namespace/1,
qualify/2,
@ -48,6 +49,7 @@
, type_vars => #{ string() => aeb_aevm_data:type() }
, constructors => #{ [string()] => integer() } %% name to tag
, options => [any()]
, payable => boolean()
}.
pp(Icode) ->
@ -65,7 +67,8 @@ new(Options) ->
, types => builtin_types()
, type_vars => #{}
, constructors => builtin_constructors()
, options => Options}.
, options => Options
, payable => false }.
builtin_types() ->
Word = fun([]) -> word end,
@ -103,6 +106,10 @@ new_env() ->
set_name(Name, Icode) ->
maps:put(contract_name, Name, Icode).
-spec set_payable(boolean(), icode()) -> icode().
set_payable(Payable, Icode) ->
maps:put(payable, Payable, Icode).
-spec set_namespace(aeso_syntax:con() | aeso_syntax:qcon(), icode()) -> icode().
set_namespace(NS, Icode) -> Icode#{ namespace => NS }.

View File

@ -57,6 +57,7 @@ decl() ->
choice(
%% Contract declaration
[ ?RULE(keyword(contract), con(), tok('='), maybe_block(decl()), {contract, _1, _2, _4})
, ?RULE(token(payable), keyword(contract), con(), tok('='), maybe_block(decl()), add_modifiers([_1], {contract, _2, _3, _5}))
, ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4})
, ?RULE(keyword(include), str(), {include, get_ann(_1), _2})
@ -81,7 +82,7 @@ fun_or_entry() ->
?RULE(keyword(entrypoint), {entrypoint, _1})]).
modifiers() ->
many(choice([token(stateful), token(private), token(public)])).
many(choice([token(stateful), token(payable), token(private), token(public)])).
add_modifiers(Mods, Entry = {entrypoint, _}, Node) ->
add_modifiers(Mods ++ [Entry], Node);

View File

@ -37,7 +37,7 @@ lexer() ->
, {"[^/*]+|[/*]", skip()} ],
Keywords = ["contract", "include", "let", "switch", "type", "record", "datatype", "if", "elif", "else", "function",
"stateful", "true", "false", "mod", "public", "entrypoint", "private", "indexed", "namespace"],
"stateful", "payable", "true", "false", "mod", "public", "entrypoint", "private", "indexed", "namespace"],
KW = string:join(Keywords, "|"),
Rules =

View File

@ -14,20 +14,22 @@ test_contract(N) ->
?assertEqual({ok, DecACI}, aeso_aci:render_aci_json(JSON)).
test_cases(1) ->
Contract = <<"contract C =\n"
" entrypoint a(i : int) = i+1\n">>,
Contract = <<"payable contract C =\n"
" payable stateful entrypoint a(i : int) = i+1\n">>,
MapACI = #{contract =>
#{name => <<"C">>,
type_defs => [],
payable => true,
functions =>
[#{name => <<"a">>,
arguments =>
[#{name => <<"i">>,
type => <<"int">>}],
returns => <<"int">>,
stateful => false}]}},
DecACI = <<"contract C =\n"
" entrypoint a : (int) => int\n">>,
stateful => true,
payable => true}]}},
DecACI = <<"payable contract C =\n"
" payable entrypoint a : (int) => int\n">>,
{Contract,MapACI,DecACI};
test_cases(2) ->
@ -35,7 +37,7 @@ test_cases(2) ->
" type allan = int\n"
" entrypoint a(i : allan) = i+1\n">>,
MapACI = #{contract =>
#{name => <<"C">>,
#{name => <<"C">>, payable => false,
type_defs =>
[#{name => <<"allan">>,
typedef => <<"int">>,
@ -46,7 +48,8 @@ test_cases(2) ->
type => <<"C.allan">>}],
name => <<"a">>,
returns => <<"int">>,
stateful => false}]}},
stateful => false,
payable => false}]}},
DecACI = <<"contract C =\n"
" type allan = int\n"
" entrypoint a : (C.allan) => int\n">>,
@ -64,8 +67,8 @@ test_cases(3) ->
type =>
#{<<"C.bert">> => [<<"string">>]}}],
name => <<"a">>,returns => <<"int">>,
stateful => false}],
name => <<"C">>,
stateful => false, payable => false}],
name => <<"C">>, payable => false,
event => #{variant => [#{<<"SingleEventDefined">> => []}]},
state => <<"unit">>,
type_defs =>

View File

@ -123,7 +123,8 @@ compilable_contracts() ->
{"bitcoin_auth", "authorize", ["1", "#0102030405060708090a0b0c0d0e0f101718192021222324252627282930313233343536373839401a1b1c1d1e1f202122232425262728293031323334353637"]},
{"bitcoin_auth", "to_sign", ["#0102030405060708090a0b0c0d0e0f1017181920212223242526272829303132", "2"]},
{"stub", "foo", ["42"]},
{"stub", "foo", ["-42"]}
{"stub", "foo", ["-42"]},
{"payable", "foo", ["42"]}
].
not_yet_compilable(fate) ->

View File

@ -125,7 +125,8 @@ compilable_contracts() ->
"stdlib_include",
"double_include",
"manual_stdlib_include",
"list_comp"
"list_comp",
"payable"
].
not_yet_compilable(fate) -> [];

View File

@ -31,3 +31,6 @@ contract AddrChain =
entrypoint c_creator() : address =
Contract.creator
entrypoint is_payable(a : address) : bool =
Address.is_payable(a)

View File

@ -0,0 +1,3 @@
payable contract Test =
payable entrypoint foo(x : int) = ()
function bar() = 42