Revamp private/public functions

Problem: having public as the default makes it very easy to accidentally
export local function by forgetting the `private` modifier.

Solution: functions are private by default and must be declared as `entrypoint`s
to be exported. So `entrypoint foo() = ...` instead of `function foo() = ...`.

We still accept the `private` modifier although it is redundant.
This commit is contained in:
Ulf Norell 2019-06-27 12:10:25 +02:00
parent dd5fc17554
commit 79137e058e
7 changed files with 110 additions and 24 deletions

View File

@ -538,6 +538,7 @@ infer(Contracts, Options) ->
Env = init_env(Options), Env = init_env(Options),
create_options(Options), create_options(Options),
ets_new(type_vars, [set]), ets_new(type_vars, [set]),
check_modifiers(Env, Contracts),
{Env1, Decls} = infer1(Env, Contracts, [], Options), {Env1, Decls} = infer1(Env, Contracts, [], Options),
{Env2, Decls2} = {Env2, Decls2} =
case proplists:get_value(dont_unfold, Options, false) of case proplists:get_value(dont_unfold, Options, false) of
@ -685,6 +686,40 @@ check_typedef(Env, {variant_t, Cons}) ->
check_unexpected(Xs) -> check_unexpected(Xs) ->
[ type_error(X) || X <- Xs ]. [ type_error(X) || X <- Xs ].
check_modifiers(Env, Contracts) ->
create_type_errors(),
[ case C of
{contract, _, Con, Decls} ->
check_modifiers1(contract, Decls),
case {lists:keymember(letfun, 1, Decls),
[ D || D <- Decls, aeso_syntax:get_ann(entrypoint, D, false) ]} of
{true, []} -> type_error({contract_has_no_entrypoints, Con});
_ -> ok
end;
{namespace, _, _, Decls} -> check_modifiers1(namespace, Decls)
end || C <- Contracts ],
destroy_and_report_type_errors(Env).
-spec check_modifiers1(contract | namespace, [aeso_syntax:decl()] | aeso_syntax:decl()) -> ok.
check_modifiers1(What, Decls) when is_list(Decls) ->
_ = [ check_modifiers1(What, Decl) || Decl <- Decls ],
ok;
check_modifiers1(What, Decl) when element(1, Decl) == letfun; element(1, Decl) == fun_decl ->
Public = aeso_syntax:get_ann(public, Decl, false),
Private = aeso_syntax:get_ann(private, Decl, false),
Entrypoint = aeso_syntax:get_ann(entrypoint, Decl, false),
FunDecl = element(1, Decl) == fun_decl,
{id, _, Name} = element(3, Decl),
_ = [ type_error({proto_must_be_entrypoint, Decl}) || FunDecl, Private orelse not Entrypoint, What == contract ],
_ = [ type_error({proto_in_namespace, Decl}) || FunDecl, What == namespace ],
_ = [ type_error({init_must_be_an_entrypoint, Decl}) || not Entrypoint, Name == "init", What == contract ],
_ = [ type_error({public_modifier_in_contract, Decl}) || Public, not Private, not Entrypoint, What == contract ],
_ = [ type_error({entrypoint_in_namespace, Decl}) || Entrypoint, What == namespace ],
_ = [ type_error({private_entrypoint, Decl}) || Private, Entrypoint ],
_ = [ type_error({private_and_public, Decl}) || Private, Public ],
ok;
check_modifiers1(_, _) -> ok.
-spec check_type(env(), aeso_syntax:type()) -> aeso_syntax:type(). -spec check_type(env(), aeso_syntax:type()) -> aeso_syntax:type().
check_type(Env, T) -> check_type(Env, T) ->
check_type(Env, T, 0). check_type(Env, T, 0).
@ -2070,7 +2105,7 @@ pp_error({duplicate_definition, Name, Locs}) ->
pp_error({duplicate_scope, Kind, Name, OtherKind, L}) -> pp_error({duplicate_scope, Kind, Name, OtherKind, L}) ->
io_lib:format("The ~p ~s (at ~s) has the same name as a ~p at ~s\n", io_lib:format("The ~p ~s (at ~s) has the same name as a ~p at ~s\n",
[Kind, pp(Name), pp_loc(Name), OtherKind, pp_loc(L)]); [Kind, pp(Name), pp_loc(Name), OtherKind, pp_loc(L)]);
pp_error({include, {string, Pos, Name}}) -> pp_error({include, _, {string, Pos, Name}}) ->
io_lib:format("Include of '~s' at ~s\nnot allowed, include only allowed at top level.\n", io_lib:format("Include of '~s' at ~s\nnot allowed, include only allowed at top level.\n",
[binary_to_list(Name), pp_loc(Pos)]); [binary_to_list(Name), pp_loc(Pos)]);
pp_error({namespace, _Pos, {con, Pos, Name}, _Def}) -> pp_error({namespace, _Pos, {con, Pos, Name}, _Def}) ->
@ -2093,9 +2128,46 @@ pp_error({init_depends_on_state, Which, [_Init | Chain]}) ->
|| {[_, Fun], Ann} <- Chain]]); || {[_, Fun], Ann} <- Chain]]);
pp_error({missing_body_for_let, Ann}) -> pp_error({missing_body_for_let, Ann}) ->
io_lib:format("Let binding at ~s must be followed by an expression\n", [pp_loc(Ann)]); io_lib:format("Let binding at ~s must be followed by an expression\n", [pp_loc(Ann)]);
pp_error({public_modifier_in_contract, Decl}) ->
Decl1 = mk_entrypoint(Decl),
io_lib:format("Use 'entrypoint' instead of 'function' for public function ~s (at ~s):\n~s\n",
[pp_expr("", element(3, Decl)), pp_loc(Decl),
prettypr:format(prettypr:nest(2, aeso_pretty:decl(Decl1)))]);
pp_error({init_must_be_an_entrypoint, Decl}) ->
Decl1 = mk_entrypoint(Decl),
io_lib:format("The init function (at ~s) must be an entrypoint:\n~s\n",
[pp_loc(Decl),
prettypr:format(prettypr:nest(2, aeso_pretty:decl(Decl1)))]);
pp_error({proto_must_be_entrypoint, Decl}) ->
Decl1 = mk_entrypoint(Decl),
io_lib:format("Use 'entrypoint' for declaration of ~s (at ~s):\n~s\n",
[pp_expr("", element(3, Decl)), pp_loc(Decl),
prettypr:format(prettypr:nest(2, aeso_pretty:decl(Decl1)))]);
pp_error({proto_in_namespace, Decl}) ->
io_lib:format("Namespaces cannot contain function prototypes (at ~s).\n",
[pp_loc(Decl)]);
pp_error({entrypoint_in_namespace, Decl}) ->
io_lib:format("Namespaces cannot contain entrypoints (at ~s). Use 'function' instead.\n",
[pp_loc(Decl)]);
pp_error({private_entrypoint, Decl}) ->
io_lib:format("The entrypoint ~s (at ~s) cannot be private. Use 'function' instead.\n",
[pp_expr("", element(3, Decl)), pp_loc(Decl)]);
pp_error({private_and_public, Decl}) ->
io_lib:format("The function ~s (at ~s) cannot be both public and private.\n",
[pp_expr("", element(3, Decl)), pp_loc(Decl)]);
pp_error({contract_has_no_entrypoints, Con}) ->
io_lib:format("The contract ~s (at ~s) has no entrypoints. Since Sophia version 3.2, public\n"
"contract functions must be declared with the 'entrypoint' keyword instead of\n"
"'function'.\n", [pp_expr("", Con), pp_loc(Con)]);
pp_error(Err) -> pp_error(Err) ->
io_lib:format("Unknown error: ~p\n", [Err]). io_lib:format("Unknown error: ~p\n", [Err]).
mk_entrypoint(Decl) ->
Ann = [entrypoint | lists:keydelete(public, 1,
lists:keydelete(private, 1,
aeso_syntax:get_ann(Decl))) -- [public, private]],
aeso_syntax:set_ann(Ann, Decl).
pp_when({todo, What}) -> io_lib:format("[TODO] ~p\n", [What]); pp_when({todo, What}) -> io_lib:format("[TODO] ~p\n", [What]);
pp_when({at, Ann}) -> io_lib:format("at ~s\n", [pp_loc(Ann)]); pp_when({at, Ann}) -> io_lib:format("at ~s\n", [pp_loc(Ann)]);
pp_when({check_typesig, Name, Inferred, Given}) -> pp_when({check_typesig, Name, Inferred, Given}) ->

View File

@ -1006,13 +1006,12 @@ add_fun_env(Env = #{ fun_env := FunEnv }, Decls) ->
Env#{ fun_env := maps:merge(FunEnv, FunEnv1) }. Env#{ fun_env := maps:merge(FunEnv, FunEnv1) }.
make_fun_name(#{ context := Context }, Ann, Name) -> make_fun_name(#{ context := Context }, Ann, Name) ->
Private = proplists:get_value(private, Ann, false) orelse Entrypoint = proplists:get_value(entrypoint, Ann, false),
proplists:get_value(internal, Ann, false),
case Context of case Context of
{main_contract, Main} -> {main_contract, Main} ->
if Private -> {local_fun, [Main, Name]}; if Name == "init" -> init;
Name == "init" -> init; Entrypoint -> {entrypoint, list_to_binary(Name)};
true -> {entrypoint, list_to_binary(Name)} true -> {local_fun, [Main, Name]}
end; end;
{namespace, Lib} -> {namespace, Lib} ->
{local_fun, [Lib, Name]} {local_fun, [Lib, Name]}

View File

@ -817,13 +817,11 @@ has_maps({list, T}) -> has_maps(T);
has_maps({tuple, Ts}) -> lists:any(fun has_maps/1, Ts); has_maps({tuple, Ts}) -> lists:any(fun has_maps/1, Ts);
has_maps({variant, Cs}) -> lists:any(fun has_maps/1, lists:append(Cs)). has_maps({variant, Cs}) -> lists:any(fun has_maps/1, lists:append(Cs)).
%% A function is private if marked 'private' or 'internal', or if it's not %% A function is private if not an 'entrypoint', or if it's not defined in the
%% defined in the main contract name space. (NOTE: changes when we introduce %% main contract name space. (NOTE: changes when we introduce inheritance).
%% inheritance).
is_private(Ann, #{ contract_name := MainContract } = Icode) -> is_private(Ann, #{ contract_name := MainContract } = Icode) ->
{_, _, CurrentNamespace} = aeso_icode:get_namespace(Icode), {_, _, CurrentNamespace} = aeso_icode:get_namespace(Icode),
proplists:get_value(private, Ann, false) orelse not proplists:get_value(entrypoint, Ann, false) orelse
proplists:get_value(internal, Ann, false) orelse
MainContract /= CurrentNamespace. MainContract /= CurrentNamespace.
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------

View File

@ -253,7 +253,7 @@ insert_call_function(Code, Call, FunName, Args, Options) ->
[ Code, [ Code,
"\n\n", "\n\n",
lists:duplicate(Ind, " "), lists:duplicate(Ind, " "),
"stateful function ", Call,"() = ", FunName, "(", string:join(Args, ","), ")\n" "stateful entrypoint ", Call, "() = ", FunName, "(", string:join(Args, ","), ")\n"
]). ]).
-spec insert_init_function(string(), options()) -> string(). -spec insert_init_function(string(), options()) -> string().
@ -263,7 +263,7 @@ insert_init_function(Code, Options) ->
lists:flatten( lists:flatten(
[ Code, [ Code,
"\n\n", "\n\n",
lists:duplicate(Ind, " "), "function init() = ()\n" lists:duplicate(Ind, " "), "entrypoint init() = ()\n"
]). ]).
last_contract_indent(Decls) -> last_contract_indent(Decls) ->

View File

@ -47,7 +47,7 @@ decl() ->
%% Contract declaration %% Contract declaration
[ ?RULE(keyword(contract), con(), tok('='), maybe_block(decl()), {contract, _1, _2, _4}) [ ?RULE(keyword(contract), con(), tok('='), maybe_block(decl()), {contract, _1, _2, _4})
, ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4}) , ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4})
, ?RULE(keyword(include), str(), {include, _2}) , ?RULE(keyword(include), str(), {include, get_ann(_1), _2})
%% Type declarations TODO: format annotation for "type bla" vs "type bla()" %% Type declarations TODO: format annotation for "type bla" vs "type bla()"
, ?RULE(keyword(type), id(), {type_decl, _1, _2, []}) , ?RULE(keyword(type), id(), {type_decl, _1, _2, []})
@ -60,13 +60,22 @@ decl() ->
, ?RULE(keyword(datatype), id(), type_vars(), tok('='), typedef(variant), {type_def, _1, _2, _3, _5}) , ?RULE(keyword(datatype), id(), type_vars(), tok('='), typedef(variant), {type_def, _1, _2, _3, _5})
%% Function declarations %% Function declarations
, ?RULE(modifiers(), keyword(function), id(), tok(':'), type(), add_modifiers(_1, {fun_decl, _2, _3, _5})) , ?RULE(modifiers(), fun_or_entry(), id(), tok(':'), type(), add_modifiers(_1, _2, {fun_decl, get_ann(_2), _3, _5}))
, ?RULE(modifiers(), keyword(function), fundef(), add_modifiers(_1, set_pos(get_pos(_2), _3))) , ?RULE(modifiers(), fun_or_entry(), fundef(), add_modifiers(_1, _2, set_pos(get_pos(get_ann(_2)), _3)))
, ?RULE(keyword('let'), valdef(), set_pos(get_pos(_1), _2)) , ?RULE(keyword('let'), valdef(), set_pos(get_pos(_1), _2))
])). ])).
fun_or_entry() ->
choice([?RULE(keyword(function), {function, _1}),
?RULE(keyword(entrypoint), {entrypoint, _1})]).
modifiers() -> modifiers() ->
many(choice([token(stateful), token(public), token(private), token(internal)])). many(choice([token(stateful), token(private), token(public)])).
add_modifiers(Mods, Entry = {entrypoint, _}, Node) ->
add_modifiers(Mods ++ [Entry], Node);
add_modifiers(Mods, {function, _}, Node) ->
add_modifiers(Mods, Node).
add_modifiers([], Node) -> Node; add_modifiers([], Node) -> Node;
add_modifiers(Mods = [Tok | _], Node) -> add_modifiers(Mods = [Tok | _], Node) ->
@ -513,7 +522,7 @@ expand_includes(AST, Opts) ->
expand_includes([], Acc, _Opts) -> expand_includes([], Acc, _Opts) ->
{ok, lists:reverse(Acc)}; {ok, lists:reverse(Acc)};
expand_includes([{include, S = {string, _, File}} | AST], Acc, Opts) -> expand_includes([{include, _, S = {string, _, File}} | AST], Acc, Opts) ->
case read_file(File, Opts) of case read_file(File, Opts) of
{ok, Bin} -> {ok, Bin} ->
Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}), Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}),

View File

@ -153,13 +153,21 @@ decl({type_decl, _, T, Vars}) -> typedecl(alias_t, T, Vars);
decl({type_def, _, T, Vars, Def}) -> decl({type_def, _, T, Vars, Def}) ->
Kind = element(1, Def), Kind = element(1, Def),
equals(typedecl(Kind, T, Vars), typedef(Def)); equals(typedecl(Kind, T, Vars), typedef(Def));
decl({fun_decl, _, F, T}) -> decl({fun_decl, Ann, F, T}) ->
hsep(text("function"), typed(name(F), T)); Fun = case aeso_syntax:get_ann(entrypoint, Ann, false) of
true -> text("entrypoint");
false -> text("function")
end,
hsep(Fun, typed(name(F), T));
decl(D = {letfun, Attrs, _, _, _, _}) -> decl(D = {letfun, Attrs, _, _, _, _}) ->
Mod = fun({Mod, true}) when Mod == private; Mod == internal; Mod == public; Mod == stateful -> Mod = fun({Mod, true}) when Mod == private; Mod == stateful ->
text(atom_to_list(Mod)); text(atom_to_list(Mod));
(_) -> empty() end, (_) -> empty() end,
hsep(lists:map(Mod, Attrs) ++ [letdecl("function", D)]); Fun = case aeso_syntax:get_ann(entrypoint, Attrs, false) of
true -> "entrypoint";
false -> "function"
end,
hsep(lists:map(Mod, Attrs) ++ [letdecl(Fun, D)]);
decl(D = {letval, _, _, _, _}) -> letdecl("let", D). decl(D = {letval, _, _, _, _}) -> letdecl("let", D).
-spec expr(aeso_syntax:expr(), options()) -> doc(). -spec expr(aeso_syntax:expr(), options()) -> doc().

View File

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