Add compiler warnings #837
@ -70,7 +70,7 @@ do_contract_interface(Type, Contract, Options) when is_binary(Contract) ->
|
||||
do_contract_interface(Type, ContractString, Options) ->
|
||||
try
|
||||
Ast = aeso_compiler:parse(ContractString, Options),
|
||||
{TypedAst, _} = aeso_ast_infer_types:infer(Ast, [dont_unfold | Options]),
|
||||
{TypedAst, _, _} = aeso_ast_infer_types:infer(Ast, [dont_unfold | Options]),
|
||||
from_typed_ast(Type, TypedAst)
|
||||
catch
|
||||
throw:{error, Errors} -> {error, Errors}
|
||||
|
@ -171,7 +171,8 @@ on_scopes(Env = #env{ scopes = Scopes }, Fun) ->
|
||||
Env#env{ scopes = maps:map(fun(_, Scope) -> Fun(Scope) end, Scopes) }.
|
||||
|
||||
-spec bind_var(aeso_syntax:id(), utype(), env()) -> env().
|
||||
bind_var({id, Ann, X}, T, Env) ->
|
||||
bind_var({id, Ann, X}, T, Env = #env{ vars = Vars }) ->
|
||||
when_warning(warn_shadowing, fun() -> warn_potential_shadowing(Ann, X, Vars) end),
|
||||
Env#env{ vars = [{X, {Ann, T}} | Env#env.vars] }.
|
||||
|
||||
-spec bind_vars([{aeso_syntax:id(), utype()}], env()) -> env().
|
||||
@ -368,7 +369,9 @@ lookup_env(Env, Kind, Ann, Name) ->
|
||||
Names = [ Qual ++ [lists:last(Name)] || Qual <- possible_scopes(Env, Name) ],
|
||||
case [ Res || QName <- Names, Res <- [lookup_env1(Env, Kind, Ann, QName)], Res /= false] of
|
||||
[] -> false;
|
||||
[Res] -> Res;
|
||||
[Res = {_, {AnnR, _}}] ->
|
||||
when_warning(warn_unused_includes, fun() -> used_include(AnnR) end),
|
||||
Res;
|
||||
Many ->
|
||||
type_error({ambiguous_name, [{qid, A, Q} || {Q, {A, _}} <- Many]}),
|
||||
false
|
||||
@ -775,7 +778,7 @@ global_env() ->
|
||||
option_t(As, T) -> {app_t, As, {id, As, "option"}, [T]}.
|
||||
map_t(As, K, V) -> {app_t, As, {id, As, "map"}, [K, V]}.
|
||||
|
||||
-spec infer(aeso_syntax:ast()) -> {aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}.
|
||||
-spec infer(aeso_syntax:ast()) -> {aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]} | {env(), aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]}.
|
||||
infer(Contracts) ->
|
||||
infer(Contracts, []).
|
||||
|
||||
@ -785,7 +788,7 @@ infer(Contracts) ->
|
||||
init_env(_Options) -> global_env().
|
||||
|
||||
-spec infer(aeso_syntax:ast(), list(option())) ->
|
||||
{aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}.
|
||||
{aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]} | {env(), aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]}.
|
||||
infer([], Options) ->
|
||||
create_type_errors(),
|
||||
type_error({no_decls, proplists:get_value(src_file, Options, no_file)}),
|
||||
@ -797,11 +800,16 @@ infer(Contracts, Options) ->
|
||||
create_options(Options),
|
||||
ets_new(defined_contracts, [bag]),
|
||||
ets_new(type_vars, [set]),
|
||||
ets_new(warnings, [bag]),
|
||||
when_warning(warn_unused_functions, fun() -> create_unused_functions() end),
|
||||
check_modifiers(Env, Contracts),
|
||||
create_type_errors(),
|
||||
Contracts1 = identify_main_contract(Contracts, Options),
|
||||
destroy_and_report_type_errors(Env),
|
||||
{Env1, Decls} = infer1(Env, Contracts1, [], Options),
|
||||
when_warning(warn_unused_functions, fun() -> destroy_and_report_unused_functions() end),
|
||||
when_option(warn_error, fun() -> destroy_and_report_warnings_as_type_errors() end),
|
||||
Warnings = lists:map(fun mk_warning/1, ets_tab2list(warnings)),
|
||||
{Env2, DeclsFolded, DeclsUnfolded} =
|
||||
case proplists:get_value(dont_unfold, Options, false) of
|
||||
true -> {Env1, Decls, Decls};
|
||||
@ -809,8 +817,8 @@ infer(Contracts, Options) ->
|
||||
{E, Decls, unfold_record_types(E, Decls)}
|
||||
end,
|
||||
case proplists:get_value(return_env, Options, false) of
|
||||
false -> {DeclsFolded, DeclsUnfolded};
|
||||
true -> {Env2, DeclsFolded, DeclsUnfolded}
|
||||
false -> {DeclsFolded, DeclsUnfolded, Warnings};
|
||||
true -> {Env2, DeclsFolded, DeclsUnfolded, Warnings}
|
||||
end
|
||||
after
|
||||
clean_up_ets()
|
||||
@ -838,6 +846,7 @@ infer1(Env, [{Contract, Ann, ConName, Code} | Rest], Acc, Options)
|
||||
Env3 = bind_contract(Contract1, Env2),
|
||||
infer1(Env3, Rest, [Contract1 | Acc], Options);
|
||||
infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) ->
|
||||
when_warning(warn_unused_includes, fun() -> potential_unused_include(Ann, proplists:get_value(src_file, Options, no_file)) end),
|
||||
check_scope_name_clash(Env, namespace, Name),
|
||||
{Env1, Code1} = infer_contract_top(push_scope(namespace, Name, Env), namespace, Code, Options),
|
||||
Namespace1 = {namespace, Ann, Name, Code1},
|
||||
@ -907,6 +916,7 @@ infer_contract(Env0, What, Defs0, Options) ->
|
||||
OldUsedNamespaces = Env#env.used_namespaces,
|
||||
Env01 = check_usings(Env, Get(using, Defs)),
|
||||
{Env1, TypeDefs} = check_typedefs(Env01, Get(type, Defs)),
|
||||
when_warning(warn_unused_typedefs, fun() -> potential_unused_typedefs(Env#env.namespace, TypeDefs) end),
|
||||
create_type_errors(),
|
||||
check_unexpected(Get(unexpected, Defs)),
|
||||
Env2 =
|
||||
@ -1345,7 +1355,10 @@ infer_letrec(Env, Defs) ->
|
||||
[print_typesig(S) || S <- TypeSigs],
|
||||
{TypeSigs, NewDefs}.
|
||||
|
||||
infer_letfun(Env, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) ->
|
||||
infer_letfun(Env = #env{ namespace = Namespace }, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) ->
|
||||
when_warning(warn_unused_stateful, fun() -> potential_unused_stateful(Ann, Fun) end),
|
||||
when_warning(warn_unused_functions,
|
||||
fun() -> potential_unused_function(Env, Ann, Namespace ++ qname(Fun), Fun) end),
|
||||
Type1 = check_type(Env, Type),
|
||||
{NameSigs, Clauses1} = lists:unzip([ infer_letfun1(Env, Clause) || Clause <- Clauses ]),
|
||||
{_, Sigs = [Sig | _]} = lists:unzip(NameSigs),
|
||||
@ -1354,13 +1367,17 @@ infer_letfun(Env, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) ->
|
||||
unify(Env, ClauseT, Type1, {check_typesig, Name, ClauseT, Type1})
|
||||
end || ClauseSig <- Sigs ],
|
||||
{{Name, Sig}, desugar_clauses(Ann, Fun, Sig, Clauses1)};
|
||||
infer_letfun(Env, LetFun = {letfun, Ann, Fun, _, _, _}) ->
|
||||
infer_letfun(Env = #env{ namespace = Namespace }, LetFun = {letfun, Ann, Fun, _, _, _}) ->
|
||||
when_warning(warn_unused_stateful, fun() -> potential_unused_stateful(Ann, Fun) end),
|
||||
when_warning(warn_unused_functions, fun() -> potential_unused_function(Env, Ann, Namespace ++ qname(Fun), Fun) end),
|
||||
{{Name, Sig}, Clause} = infer_letfun1(Env, LetFun),
|
||||
{{Name, Sig}, desugar_clauses(Ann, Fun, Sig, [Clause])}.
|
||||
infer_letfun1(Env0, {letfun, Attrib, Fun = {id, NameAttrib, Name}, Args, What, GuardedBodies}) ->
|
||||
|
||||
infer_letfun1(Env0 = #env{ namespace = NS }, {letfun, Attrib, Fun = {id, NameAttrib, Name}, Args, What, GuardedBodies}) ->
|
||||
Env = Env0#env{ stateful = aeso_syntax:get_ann(stateful, Attrib, false),
|
||||
current_function = Fun },
|
||||
{NewEnv, {typed, _, {tuple, _, TypedArgs}, {tuple_t, _, ArgTypes}}} = infer_pattern(Env, {tuple, [{origin, system} | NameAttrib], Args}),
|
||||
when_warning(warn_unused_variables, fun() -> potential_unused_variables(NS, Name, free_vars(Args)) end),
|
||||
ExpectedType = check_type(Env, arg_type(NameAttrib, What)),
|
||||
InferGuardedBodies = fun({guarded, Ann, Guards, Body}) ->
|
||||
NewGuards = lists:map(fun(Guard) ->
|
||||
@ -1415,12 +1432,13 @@ app_t(Ann, Name, Args) -> {app_t, Ann, Name, Args}.
|
||||
lookup_name(Env, As, Name) ->
|
||||
lookup_name(Env, As, Name, []).
|
||||
|
||||
lookup_name(Env, As, Id, Options) ->
|
||||
lookup_name(Env = #env{ namespace = NS, current_function = {id, _, Fun} }, As, Id, Options) ->
|
||||
case lookup_env(Env, term, As, qname(Id)) of
|
||||
false ->
|
||||
type_error({unbound_variable, Id}),
|
||||
{Id, fresh_uvar(As)};
|
||||
{QId, {_, Ty}} ->
|
||||
when_warning(warn_unused_variables, fun() -> used_variable(NS, Fun, QId) end),
|
||||
Freshen = proplists:get_value(freshen, Options, false),
|
||||
check_stateful(Env, Id, Ty),
|
||||
Ty1 = case Ty of
|
||||
@ -1443,7 +1461,9 @@ check_stateful(#env{ stateful = false, current_function = Fun }, Id, Type = {typ
|
||||
true ->
|
||||
type_error({stateful_not_allowed, Id, Fun})
|
||||
end;
|
||||
check_stateful(_Env, _Id, _Type) -> ok.
|
||||
check_stateful(#env { current_function = Fun }, _Id, _Type) ->
|
||||
when_warning(warn_unused_stateful, fun() -> used_stateful(Fun) end),
|
||||
ok.
|
||||
|
||||
%% Hack: don't allow passing the 'value' named arg if not stateful. This only
|
||||
%% works since the user can't create functions with named arguments.
|
||||
@ -1601,16 +1621,21 @@ infer_expr(Env, {app, Ann, Fun, Args0} = App) ->
|
||||
prefix ->
|
||||
infer_op(Env, Ann, Fun, Args, fun infer_prefix/1);
|
||||
_ ->
|
||||
CurrentFun = Env#env.current_function,
|
||||
Namespace = Env#env.namespace,
|
||||
NamedArgsVar = fresh_uvar(Ann),
|
||||
NamedArgs1 = [ infer_named_arg(Env, NamedArgsVar, Arg) || Arg <- NamedArgs ],
|
||||
NewFun0 = infer_expr(Env, Fun),
|
||||
NewArgs = [infer_expr(Env, A) || A <- Args],
|
||||
ArgTypes = [T || {typed, _, _, T} <- NewArgs],
|
||||
NewFun1 = {typed, _, _, FunType} = infer_var_args_fun(Env, NewFun0, NamedArgs1, ArgTypes),
|
||||
NewFun1 = {typed, _, Name, FunType} = infer_var_args_fun(Env, NewFun0, NamedArgs1, ArgTypes),
|
||||
When = {infer_app, Fun, NamedArgs1, Args, FunType, ArgTypes},
|
||||
GeneralResultType = fresh_uvar(Ann),
|
||||
ResultType = fresh_uvar(Ann),
|
||||
when_warning(warn_unused_functions,
|
||||
fun() -> register_function_call(Namespace ++ qname(CurrentFun), Name) end),
|
||||
unify(Env, FunType, {fun_t, [], NamedArgsVar, ArgTypes, GeneralResultType}, When),
|
||||
when_warning(warn_negative_spend, fun() -> warn_potential_negative_spend(Ann, NewFun1, NewArgs) end),
|
||||
add_named_argument_constraint(
|
||||
#dependent_type_constraint{ named_args_t = NamedArgsVar,
|
||||
named_args = NamedArgs1,
|
||||
@ -1835,6 +1860,7 @@ infer_op(Env, As, Op, Args, InferOp) ->
|
||||
ArgTypes = [T || {typed, _, _, T} <- TypedArgs],
|
||||
Inferred = {fun_t, _, _, OperandTypes, ResultType} = InferOp(Op),
|
||||
unify(Env, ArgTypes, OperandTypes, {infer_app, Op, [], Args, Inferred, ArgTypes}),
|
||||
when_warning(warn_division_by_zero, fun() -> warn_potential_division_by_zero(As, Op, Args) end),
|
||||
{typed, As, {app, As, Op, TypedArgs}, ResultType}.
|
||||
|
||||
infer_pattern(Env, Pattern) ->
|
||||
@ -1848,8 +1874,9 @@ infer_pattern(Env, Pattern) ->
|
||||
NewPattern = infer_expr(NewEnv, Pattern),
|
||||
{NewEnv#env{ in_pattern = Env#env.in_pattern }, NewPattern}.
|
||||
|
||||
infer_case(Env, Attrs, Pattern, ExprType, GuardedBranches, SwitchType) ->
|
||||
infer_case(Env = #env{ namespace = NS, current_function = {id, _, Fun} }, Attrs, Pattern, ExprType, GuardedBranches, SwitchType) ->
|
||||
{NewEnv, NewPattern = {typed, _, _, PatType}} = infer_pattern(Env, Pattern),
|
||||
when_warning(warn_unused_variables, fun() -> potential_unused_variables(NS, Fun, free_vars(Pattern)) end),
|
||||
InferGuardedBranches = fun({guarded, Ann, Guards, Branch}) ->
|
||||
NewGuards = lists:map(fun(Guard) ->
|
||||
check_expr(NewEnv#env{ in_guard = true }, Guard, {id, Attrs, "bool"})
|
||||
@ -1879,7 +1906,9 @@ infer_block(Env, _, [{letval, Attrs, Pattern, E}|Rest], BlockType) ->
|
||||
infer_block(Env, Attrs, [Using = {using, _, _, _, _} | Rest], BlockType) ->
|
||||
infer_block(check_usings(Env, Using), Attrs, Rest, BlockType);
|
||||
infer_block(Env, Attrs, [E|Rest], BlockType) ->
|
||||
[infer_expr(Env, E)|infer_block(Env, Attrs, Rest, BlockType)].
|
||||
NewE = infer_expr(Env, E),
|
||||
when_warning(warn_unused_return_value, fun() -> potential_unused_return_value(NewE) end),
|
||||
[NewE|infer_block(Env, Attrs, Rest, BlockType)].
|
||||
|
||||
infer_infix({BoolOp, As})
|
||||
when BoolOp =:= '&&'; BoolOp =:= '||' ->
|
||||
@ -1959,7 +1988,8 @@ next_count() ->
|
||||
|
||||
ets_tables() ->
|
||||
[options, type_vars, type_defs, record_fields, named_argument_constraints,
|
||||
field_constraints, freshen_tvars, type_errors, defined_contracts].
|
||||
field_constraints, freshen_tvars, type_errors, defined_contracts,
|
||||
warnings, function_calls, all_functions].
|
||||
|
||||
clean_up_ets() ->
|
||||
[ catch ets_delete(Tab) || Tab <- ets_tables() ],
|
||||
@ -1971,6 +2001,13 @@ clean_up_ets() ->
|
||||
ets_init() ->
|
||||
put(aeso_ast_infer_types, #{}).
|
||||
|
||||
ets_tab_exists(Name) ->
|
||||
Tabs = get(aeso_ast_infer_types),
|
||||
case maps:find(Name, Tabs) of
|
||||
{ok, _} -> true;
|
||||
error -> false
|
||||
end.
|
||||
|
||||
ets_tabid(Name) ->
|
||||
#{Name := TabId} = get(aeso_ast_infer_types),
|
||||
TabId.
|
||||
@ -1996,6 +2033,10 @@ ets_lookup(Name, Key) ->
|
||||
TabId = ets_tabid(Name),
|
||||
ets:lookup(TabId, Key).
|
||||
|
||||
ets_match_delete(Name, Pattern) ->
|
||||
TabId = ets_tabid(Name),
|
||||
ets:match_delete(TabId, Pattern).
|
||||
|
||||
ets_tab2list(Name) ->
|
||||
TabId = ets_tabid(Name),
|
||||
ets:tab2list(TabId).
|
||||
@ -2461,6 +2502,7 @@ unfold_types_in_type(Env, {app_t, Ann, Id = {id, _, "map"}, Args = [KeyType0, _]
|
||||
[ type_error({map_in_map_key, Ann1, KeyType0}) || has_maps(KeyType) ],
|
||||
{app_t, Ann, Id, Args1};
|
||||
unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) ->
|
||||
when_warning(warn_unused_typedefs, fun() -> used_typedef(Id, length(Args)) end),
|
||||
UnfoldRecords = proplists:get_value(unfold_record_types, Options, false),
|
||||
UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false),
|
||||
case lookup_type(Env, Id) of
|
||||
@ -2481,6 +2523,7 @@ unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id)
|
||||
end;
|
||||
unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) ->
|
||||
%% Like the case above, but for types without parameters.
|
||||
when_warning(warn_unused_typedefs, fun() -> used_typedef(Id, 0) end),
|
||||
UnfoldRecords = proplists:get_value(unfold_record_types, Options, false),
|
||||
UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false),
|
||||
case lookup_type(Env, Id) of
|
||||
@ -2733,6 +2776,154 @@ integer_to_tvar(X) when X < 26 ->
|
||||
integer_to_tvar(X) ->
|
||||
[integer_to_tvar(X div 26)] ++ [$a + (X rem 26)].
|
||||
|
||||
%% Warnings
|
||||
|
||||
all_warnings() ->
|
||||
[ warn_unused_includes
|
||||
, warn_unused_stateful
|
||||
, warn_unused_variables
|
||||
, warn_unused_typedefs
|
||||
, warn_unused_return_value
|
||||
, warn_unused_functions
|
||||
, warn_shadowing
|
||||
, warn_division_by_zero
|
||||
, warn_negative_spend ].
|
||||
|
||||
when_warning(Warn, Do) ->
|
||||
case lists:member(Warn, all_warnings()) of
|
||||
false ->
|
||||
create_type_errors(),
|
||||
type_error({unknown_warning, Warn}),
|
||||
destroy_and_report_type_errors(global_env());
|
||||
true ->
|
||||
case ets_tab_exists(warnings) of
|
||||
true ->
|
||||
IsEnabled = get_option(Warn, false),
|
||||
IsAll = get_option(warn_all, false) andalso lists:member(Warn, all_warnings()),
|
||||
if
|
||||
IsEnabled orelse IsAll -> Do();
|
||||
true -> ok
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end.
|
||||
|
||||
%% Warnings (Unused includes)
|
||||
|
||||
potential_unused_include(Ann, SrcFile) ->
|
||||
case aeso_syntax:get_ann(file, Ann, no_file) of
|
||||
no_file -> ok;
|
||||
File -> ets_insert(warnings, {unused_include, File, SrcFile})
|
||||
end.
|
||||
|
||||
used_include(Ann) ->
|
||||
case aeso_syntax:get_ann(file, Ann, no_file) of
|
||||
no_file -> ok;
|
||||
File -> ets_match_delete(warnings, {unused_include, File, '_'})
|
||||
end.
|
||||
|
||||
%% Warnings (Unused stateful)
|
||||
|
||||
potential_unused_stateful(Ann, Fun) ->
|
||||
case aeso_syntax:get_ann(stateful, Ann, false) of
|
||||
false -> ok;
|
||||
true -> ets_insert(warnings, {unused_stateful, Ann, Fun})
|
||||
end.
|
||||
|
||||
used_stateful(Fun) ->
|
||||
ets_match_delete(warnings, {unused_stateful, '_', Fun}).
|
||||
|
||||
%% Warnings (Unused type defs)
|
||||
|
||||
potential_unused_typedefs(Namespace, TypeDefs) ->
|
||||
lists:map(fun({type_def, Ann, Id, Args, _}) ->
|
||||
ets_insert(warnings, {unused_typedef, Ann, Namespace ++ qname(Id), length(Args)}) end, TypeDefs).
|
||||
|
||||
used_typedef(TypeAliasId, Arity) ->
|
||||
ets_match_delete(warnings, {unused_typedef, '_', qname(TypeAliasId), Arity}).
|
||||
|
||||
%% Warnings (Unused variables)
|
||||
|
||||
potential_unused_variables(Namespace, Fun, Vars0) ->
|
||||
Vars = [ Var || Var = {id, _, VarName} <- Vars0, VarName /= "_" ],
|
||||
lists:map(fun({id, Ann, VarName}) ->
|
||||
ets_insert(warnings, {unused_variable, Ann, Namespace, Fun, VarName}) end, Vars).
|
||||
|
||||
used_variable(Namespace, Fun, [VarName]) ->
|
||||
ets_match_delete(warnings, {unused_variable, '_', Namespace, Fun, VarName});
|
||||
used_variable(_, _, _) -> ok.
|
||||
|
||||
%% Warnings (Unused return value)
|
||||
|
||||
potential_unused_return_value({typed, Ann, {app, _, {typed, _, _, {fun_t, _, _, _, {id, _, Type}}}, _}, _}) when Type /= "unit" ->
|
||||
ets_insert(warnings, {unused_return_value, Ann});
|
||||
potential_unused_return_value(_) -> ok.
|
||||
|
||||
%% Warnings (Unused functions)
|
||||
|
||||
create_unused_functions() ->
|
||||
ets_new(function_calls, [bag]),
|
||||
ets_new(all_functions, [set]).
|
||||
|
||||
register_function_call(_Caller, {proj, _, _, _}) -> ok;
|
||||
register_function_call(Caller, Callee) ->
|
||||
ets_insert(function_calls, {Caller, qname(Callee)}).
|
||||
|
||||
potential_unused_function(#env{ what = namespace }, Ann, FunQName, FunId) ->
|
||||
ets_insert(all_functions, {Ann, FunQName, FunId, not aeso_syntax:get_ann(private, Ann, false)});
|
||||
potential_unused_function(_Env, Ann, FunQName, FunId) ->
|
||||
ets_insert(all_functions, {Ann, FunQName, FunId, aeso_syntax:get_ann(entrypoint, Ann, false)}).
|
||||
|
||||
remove_used_funs(All) ->
|
||||
{Used, Unused} = lists:partition(fun({_, _, _, IsUsed}) -> IsUsed end, All),
|
||||
CallsByUsed = lists:flatmap(fun({_, F, _, _}) -> ets_lookup(function_calls, F) end, Used),
|
||||
CalledFuns = sets:from_list(lists:map(fun({_, Callee}) -> Callee end, CallsByUsed)),
|
||||
MarkUsedFun = fun(Fun, Acc) ->
|
||||
case lists:keyfind(Fun, 2, Acc) of
|
||||
false -> Acc;
|
||||
T -> lists:keyreplace(Fun, 2, Acc, setelement(4, T, true))
|
||||
end
|
||||
end,
|
||||
NewUnused = sets:fold(MarkUsedFun, Unused, CalledFuns),
|
||||
case lists:keyfind(true, 4, NewUnused) of
|
||||
false -> NewUnused;
|
||||
_ -> remove_used_funs(NewUnused)
|
||||
end.
|
||||
|
||||
destroy_and_report_unused_functions() ->
|
||||
AllFuns = ets_tab2list(all_functions),
|
||||
lists:map(fun({Ann, _, FunId, _}) -> ets_insert(warnings, {unused_function, Ann, name(FunId)}) end,
|
||||
remove_used_funs(AllFuns)),
|
||||
ets_delete(all_functions),
|
||||
ets_delete(function_calls).
|
||||
|
||||
%% Warnings (Shadowing)
|
||||
|
||||
warn_potential_shadowing(_, "_", _) -> ok;
|
||||
warn_potential_shadowing(Ann, Name, Vars) ->
|
||||
case proplists:get_value(Name, Vars, false) of
|
||||
false -> ok;
|
||||
{AnnOld, _} -> ets_insert(warnings, {shadowing, Ann, Name, AnnOld})
|
||||
end.
|
||||
|
||||
%% Warnings (Division by zero)
|
||||
|
||||
warn_potential_division_by_zero(Ann, Op, Args) ->
|
||||
case {Op, Args} of
|
||||
{{'/', _}, [_, {int, _, 0}]} -> ets_insert(warnings, {division_by_zero, Ann});
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
%% Warnings (Negative spends)
|
||||
|
||||
warn_potential_negative_spend(Ann, Fun, Args) ->
|
||||
case {Fun, Args} of
|
||||
{ {typed, _, {qid, _, ["Chain", "spend"]}, _}
|
||||
, [_, {typed, _, {app, _, {'-', _}, [{typed, _, {int, _, X}, _}]}, _}]} when X > 0 ->
|
||||
ets_insert(warnings, {negative_spend, Ann});
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
%% Save unification failures for error messages.
|
||||
|
||||
@ -2752,6 +2943,11 @@ destroy_and_report_type_errors(Env) ->
|
||||
Errors = [ mk_error(unqualify(Env, Err)) || Err <- Errors0 ],
|
||||
aeso_errors:throw(Errors). %% No-op if Errors == []
|
||||
|
||||
destroy_and_report_warnings_as_type_errors() ->
|
||||
Warnings = [ mk_warning(Warn) || Warn <- ets_tab2list(warnings) ],
|
||||
Errors = lists:map(fun mk_t_err_from_warn/1, Warnings),
|
||||
aeso_errors:throw(Errors). %% No-op if Warnings == []
|
||||
|
||||
%% Strip current namespace from error message for nicer printing.
|
||||
unqualify(#env{ namespace = NS }, {qid, Ann, Xs}) ->
|
||||
qid(Ann, unqualify1(NS, Xs));
|
||||
@ -2774,6 +2970,9 @@ mk_t_err(Pos, Msg) ->
|
||||
mk_t_err(Pos, Msg, Ctxt) ->
|
||||
aeso_errors:new(type_error, Pos, lists:flatten(Msg), lists:flatten(Ctxt)).
|
||||
|
||||
mk_t_err_from_warn(Warn) ->
|
||||
aeso_warnings:warn_to_err(type_error, Warn).
|
||||
|
||||
mk_error({no_decls, File}) ->
|
||||
Pos = aeso_errors:pos(File, 0, 0),
|
||||
mk_t_err(Pos, "Empty contract\n");
|
||||
@ -3112,10 +3311,44 @@ mk_error({using_undefined_namespace_parts, Ann, Namespace, Parts}) ->
|
||||
PartsStr = lists:concat(lists:join(", ", Parts)),
|
||||
Msg = io_lib:format("The namespace ~s does not define the following names: ~s", [Namespace, PartsStr]),
|
||||
mk_t_err(pos(Ann), Msg);
|
||||
mk_error({unknown_warning, Warning}) ->
|
||||
Msg = io_lib:format("Trying to report unknown warning: ~p", [Warning]),
|
||||
mk_t_err(pos(0, 0), Msg);
|
||||
mk_error(Err) ->
|
||||
Msg = io_lib:format("Unknown error: ~p\n", [Err]),
|
||||
mk_t_err(pos(0, 0), Msg).
|
||||
|
||||
mk_warning({unused_include, FileName, SrcFile}) ->
|
||||
Msg = io_lib:format("The file ~s is included but not used", [FileName]),
|
||||
aeso_warnings:new(aeso_errors:pos(SrcFile, 0, 0), Msg);
|
||||
mk_warning({unused_stateful, Ann, FunName}) ->
|
||||
Msg = io_lib:format("The function ~s is unnecessarily marked as stateful at ~s", [name(FunName), pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({unused_variable, Ann, _Namespace, _Fun, VarName}) ->
|
||||
Msg = io_lib:format("The variable ~s is defined at ~s but never used", [VarName, pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({unused_typedef, Ann, QName, _Arity}) ->
|
||||
Msg = io_lib:format("The type ~s is defined at ~s but never used", [lists:last(QName), pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({unused_return_value, Ann}) ->
|
||||
Msg = io_lib:format("Unused return value at ~s", [pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({unused_function, Ann, FunName}) ->
|
||||
Msg = io_lib:format("The function ~s is defined at ~s but never used", [FunName, pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({shadowing, Ann, VarName, AnnOld}) ->
|
||||
Msg = io_lib:format("The definition of ~s at ~s shadows an older definition at ~s", [VarName, pp_loc(Ann), pp_loc(AnnOld)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({division_by_zero, Ann}) ->
|
||||
Msg = io_lib:format("Division by zero at ~s", [pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning({negative_spend, Ann}) ->
|
||||
Msg = io_lib:format("Negative spend at ~s", [pp_loc(Ann)]),
|
||||
aeso_warnings:new(pos(Ann), Msg);
|
||||
mk_warning(Warn) ->
|
||||
Msg = io_lib:format("Unknown warning: ~p\n", [Warn]),
|
||||
aeso_warnings:new(Msg).
|
||||
|
||||
mk_entrypoint(Decl) ->
|
||||
Ann = [entrypoint | lists:keydelete(public, 1,
|
||||
lists:keydelete(private, 1,
|
||||
|
@ -120,7 +120,8 @@ from_string(Backend, ContractString, Options) ->
|
||||
|
||||
from_string1(aevm, ContractString, Options) ->
|
||||
#{ icode := Icode
|
||||
, folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options),
|
||||
, folded_typed_ast := FoldedTypedAst
|
||||
, warnings := Warnings } = string_to_code(ContractString, Options),
|
||||
TypeInfo = extract_type_info(Icode),
|
||||
Assembler = assemble(Icode, Options),
|
||||
pp_assembler(aevm, Assembler, Options),
|
||||
@ -133,13 +134,15 @@ from_string1(aevm, ContractString, Options) ->
|
||||
contract_source => ContractString,
|
||||
type_info => TypeInfo,
|
||||
abi_version => aeb_aevm_abi:abi_version(),
|
||||
payable => maps:get(payable, Icode)
|
||||
payable => maps:get(payable, Icode),
|
||||
warnings => Warnings
|
||||
},
|
||||
{ok, maybe_generate_aci(Res, FoldedTypedAst, Options)};
|
||||
from_string1(fate, ContractString, Options) ->
|
||||
#{ fcode := FCode
|
||||
, fcode_env := #{child_con_env := ChildContracts}
|
||||
, folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options),
|
||||
, folded_typed_ast := FoldedTypedAst
|
||||
, warnings := Warnings } = string_to_code(ContractString, Options),
|
||||
FateCode = aeso_fcode_to_fate:compile(ChildContracts, FCode, Options),
|
||||
pp_assembler(fate, FateCode, Options),
|
||||
ByteCode = aeb_fate_code:serialize(FateCode, []),
|
||||
@ -150,7 +153,8 @@ from_string1(fate, ContractString, Options) ->
|
||||
type_info => [],
|
||||
fate_code => FateCode,
|
||||
abi_version => aeb_fate_abi:abi_version(),
|
||||
payable => maps:get(payable, FCode)
|
||||
payable => maps:get(payable, FCode),
|
||||
warnings => Warnings
|
||||
},
|
||||
{ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}.
|
||||
|
||||
@ -168,7 +172,7 @@ string_to_code(ContractString, Options) ->
|
||||
Ast = parse(ContractString, Options),
|
||||
pp_sophia_code(Ast, Options),
|
||||
pp_ast(Ast, Options),
|
||||
{TypeEnv, FoldedTypedAst, UnfoldedTypedAst} = aeso_ast_infer_types:infer(Ast, [return_env | Options]),
|
||||
{TypeEnv, FoldedTypedAst, UnfoldedTypedAst, Warnings} = aeso_ast_infer_types:infer(Ast, [return_env | Options]),
|
||||
pp_typed_ast(UnfoldedTypedAst, Options),
|
||||
case proplists:get_value(backend, Options, aevm) of
|
||||
aevm ->
|
||||
@ -178,7 +182,8 @@ string_to_code(ContractString, Options) ->
|
||||
, unfolded_typed_ast => UnfoldedTypedAst
|
||||
, folded_typed_ast => FoldedTypedAst
|
||||
, type_env => TypeEnv
|
||||
, ast => Ast };
|
||||
, ast => Ast
|
||||
, warnings => Warnings};
|
||||
fate ->
|
||||
{Env, Fcode} = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, [{original_src, ContractString}|Options]),
|
||||
#{ fcode => Fcode
|
||||
@ -186,7 +191,8 @@ string_to_code(ContractString, Options) ->
|
||||
, unfolded_typed_ast => UnfoldedTypedAst
|
||||
, folded_typed_ast => FoldedTypedAst
|
||||
, type_env => TypeEnv
|
||||
, ast => Ast }
|
||||
, ast => Ast
|
||||
, warnings => Warnings }
|
||||
end.
|
||||
|
||||
-define(CALL_NAME, "__call").
|
||||
|
@ -36,6 +36,7 @@
|
||||
, pos/2
|
||||
, pos/3
|
||||
, pp/1
|
||||
, pp_pos/1
|
||||
, to_json/1
|
||||
, throw/1
|
||||
, type/1
|
||||
|
@ -15,7 +15,8 @@
|
||||
many/1, many1/1, sep/2, sep1/2,
|
||||
infixl/2, infixr/2]).
|
||||
|
||||
-export([current_file/0, set_current_file/1]).
|
||||
-export([current_file/0, set_current_file/1,
|
||||
current_include_type/0, set_current_include_type/1]).
|
||||
|
||||
%% -- Types ------------------------------------------------------------------
|
||||
|
||||
@ -465,6 +466,13 @@ merge_with(Fun, Map1, Map2) ->
|
||||
end, Map2, maps:to_list(Map1))
|
||||
end.
|
||||
|
||||
%% Current include type
|
||||
current_include_type() ->
|
||||
get('$current_include_type').
|
||||
|
||||
set_current_include_type(IncludeType) ->
|
||||
put('$current_include_type', IncludeType).
|
||||
|
||||
%% Current source file
|
||||
current_file() ->
|
||||
get('$current_file').
|
||||
|
@ -18,7 +18,8 @@
|
||||
run_parser/3]).
|
||||
|
||||
-include("aeso_parse_lib.hrl").
|
||||
-import(aeso_parse_lib, [current_file/0, set_current_file/1]).
|
||||
-import(aeso_parse_lib, [current_file/0, set_current_file/1,
|
||||
current_include_type/0, set_current_include_type/1]).
|
||||
|
||||
-type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none().
|
||||
|
||||
@ -57,6 +58,7 @@ run_parser(P, Inp, Opts) ->
|
||||
|
||||
parse_and_scan(P, S, Opts) ->
|
||||
set_current_file(proplists:get_value(src_file, Opts, no_file)),
|
||||
set_current_include_type(proplists:get_value(include_type, Opts, none)),
|
||||
case aeso_scan:scan(S) of
|
||||
{ok, Tokens} -> aeso_parse_lib:parse(P, Tokens);
|
||||
{error, {{Input, Pos}, _}} ->
|
||||
@ -523,7 +525,11 @@ bracket_list(P) -> brackets(comma_sep(P)).
|
||||
-type ann_col() :: aeso_syntax:ann_col().
|
||||
|
||||
-spec pos_ann(ann_line(), ann_col()) -> ann().
|
||||
pos_ann(Line, Col) -> [{file, current_file()}, {line, Line}, {col, Col}].
|
||||
pos_ann(Line, Col) ->
|
||||
[ {file, current_file()}
|
||||
, {include_type, current_include_type()}
|
||||
, {line, Line}
|
||||
, {col, Col} ].
|
||||
|
||||
ann_pos(Ann) ->
|
||||
{proplists:get_value(file, Ann),
|
||||
@ -665,9 +671,14 @@ expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Op
|
||||
Hashed = hash_include(File, Code),
|
||||
case sets:is_element(Hashed, Included) of
|
||||
false ->
|
||||
IncludeType = case proplists:get_value(file, Ann) of
|
||||
no_file -> direct;
|
||||
_ -> indirect
|
||||
end,
|
||||
Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}),
|
||||
Opts2 = lists:keystore(include_type, 1, Opts1, {include_type, IncludeType}),
|
||||
Included1 = sets:add_element(Hashed, Included),
|
||||
case parse_and_scan(file(), Code, Opts1) of
|
||||
case parse_and_scan(file(), Code, Opts2) of
|
||||
{ok, AST1} ->
|
||||
expand_includes(AST1 ++ AST, Included1, Acc, Opts);
|
||||
Err = {error, _} ->
|
||||
|
27
src/aeso_warnings.erl
Normal file
27
src/aeso_warnings.erl
Normal file
@ -0,0 +1,27 @@
|
||||
-module(aeso_warnings).
|
||||
|
||||
-record(warn, { pos :: aeso_errors:pos()
|
||||
, message :: iolist()
|
||||
}).
|
||||
|
||||
-opaque warning() :: #warn{}.
|
||||
|
||||
-export_type([warning/0]).
|
||||
|
||||
-export([ new/1
|
||||
, new/2
|
||||
, warn_to_err/2
|
||||
, pp/1
|
||||
]).
|
||||
|
||||
new(Msg) ->
|
||||
new(aeso_errors:pos(0, 0), Msg).
|
||||
|
||||
new(Pos, Msg) ->
|
||||
#warn{ pos = Pos, message = Msg }.
|
||||
|
||||
warn_to_err(Kind, #warn{ pos = Pos, message = Msg }) ->
|
||||
aeso_errors:new(Kind, Pos, lists:flatten(Msg)).
|
||||
|
||||
pp(#warn{ pos = Pos, message = Msg }) ->
|
||||
lists:flatten(io_lib:format("Warning~s:\n~s", [aeso_errors:pp_pos(Pos), Msg])).
|
@ -46,7 +46,7 @@ simple_compile_test_() ->
|
||||
end} ] ++
|
||||
[ {"Testing error messages of " ++ ContractName,
|
||||
fun() ->
|
||||
Errors = compile(aevm, ContractName),
|
||||
Errors = compile(aevm, ContractName, [warn_all, warn_error]),
|
||||
check_errors(ExpectedErrors, Errors)
|
||||
end} ||
|
||||
{ContractName, ExpectedErrors} <- failing_contracts() ] ++
|
||||
@ -88,6 +88,11 @@ simple_compile_test_() ->
|
||||
?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}),
|
||||
ok
|
||||
end} || Backend <- [aevm, fate] ] ++
|
||||
[ {"Testing warning messages",
|
||||
fun() ->
|
||||
#{ warnings := Warnings } = compile(Backend, "warnings", [warn_all]),
|
||||
check_warnings(warnings(), Warnings)
|
||||
end} || Backend <- [aevm, fate] ] ++
|
||||
[].
|
||||
|
||||
%% Check if all modules in the standard library compile
|
||||
@ -119,6 +124,15 @@ check_errors(Expect0, Actual0) ->
|
||||
{Missing, Extra} -> ?assertEqual(Missing, Extra)
|
||||
end.
|
||||
|
||||
check_warnings(Expect0, Actual0) ->
|
||||
Expect = lists:sort(Expect0),
|
||||
Actual = [ list_to_binary(string:trim(aeso_warnings:pp(Warn))) || Warn <- Actual0 ],
|
||||
case {Expect -- Actual, Actual -- Expect} of
|
||||
{[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra});
|
||||
{Missing, []} -> ?assertMatch({missing, []}, {missing, Missing});
|
||||
{Missing, Extra} -> ?assertEqual(Missing, Extra)
|
||||
end.
|
||||
|
||||
compile(Backend, Name) ->
|
||||
compile(Backend, Name,
|
||||
[{include, {file_system, [aeso_test_utils:contract_path()]}}]).
|
||||
@ -228,6 +242,49 @@ debug_mode_contracts() ->
|
||||
-define(TYPE_ERROR(Name, Errs), ?ERROR("Type", Name, Errs)).
|
||||
-define(PARSE_ERROR(Name, Errs), ?ERROR("Parse", Name, Errs)).
|
||||
|
||||
-define(PosW(Kind, File, Line, Col), (list_to_binary(Kind))/binary, " in '",
|
||||
(list_to_binary(File))/binary, ".aes' at line " ??Line ", col " ??Col ":\n").
|
||||
-define(PosW(Line, Col), ?PosW(__Kind, __File, Line, Col)).
|
||||
|
||||
-define(WARNING(Name, Warns),
|
||||
(fun() ->
|
||||
__Kind = "Warning",
|
||||
__File = ??Name,
|
||||
Warns
|
||||
end)()).
|
||||
|
||||
warnings() ->
|
||||
?WARNING(warnings,
|
||||
[<<?PosW(0, 0)
|
||||
"The file Triple.aes is included but not used">>,
|
||||
<<?PosW(13, 3)
|
||||
"The function h is defined at line 13, column 3 but never used">>,
|
||||
<<?PosW(19, 3)
|
||||
"The type unused_type is defined at line 19, column 3 but never used">>,
|
||||
<<?PosW(23, 54)
|
||||
"Negative spend at line 23, column 54">>,
|
||||
<<?PosW(27, 9)
|
||||
"The definition of x at line 27, column 9 shadows an older definition at line 26, column 9">>,
|
||||
<<?PosW(30, 36)
|
||||
"Division by zero at line 30, column 36">>,
|
||||
<<?PosW(32, 3)
|
||||
"The function unused_stateful is unnecessarily marked as stateful at line 32, column 3">>,
|
||||
<<?PosW(35, 31)
|
||||
"The variable unused_arg is defined at line 35, column 31 but never used">>,
|
||||
<<?PosW(36, 9)
|
||||
"The variable unused_var is defined at line 36, column 9 but never used">>,
|
||||
<<?PosW(41, 3)
|
||||
"The function unused_function is defined at line 41, column 3 but never used">>,
|
||||
<<?PosW(42, 3)
|
||||
"The function recursive_unused_function is defined at line 42, column 3 but never used">>,
|
||||
<<?PosW(43, 3)
|
||||
"The function called_unused_function1 is defined at line 43, column 3 but never used">>,
|
||||
<<?PosW(44, 3)
|
||||
"The function called_unused_function2 is defined at line 44, column 3 but never used">>,
|
||||
<<?PosW(48, 5)
|
||||
"Unused return value at line 48, column 5">>
|
||||
]).
|
||||
|
||||
failing_contracts() ->
|
||||
{ok, V} = aeso_compiler:numeric_version(),
|
||||
Version = list_to_binary(string:join([integer_to_list(N) || N <- V], ".")),
|
||||
@ -817,6 +874,36 @@ failing_contracts() ->
|
||||
[<<?Pos(4,24)
|
||||
"Cannot unify string\n and bool\nwhen checking the type of the expression at line 4, column 24\n \"y\" : string\nagainst the expected type\n bool">>
|
||||
])
|
||||
, ?TYPE_ERROR(warnings,
|
||||
[<<?Pos(0, 0)
|
||||
"The file Triple.aes is included but not used">>,
|
||||
<<?Pos(13, 3)
|
||||
"The function h is defined at line 13, column 3 but never used">>,
|
||||
<<?Pos(19, 3)
|
||||
"The type unused_type is defined at line 19, column 3 but never used">>,
|
||||
<<?Pos(23, 54)
|
||||
"Negative spend at line 23, column 54">>,
|
||||
<<?Pos(27, 9)
|
||||
"The definition of x at line 27, column 9 shadows an older definition at line 26, column 9">>,
|
||||
<<?Pos(30, 36)
|
||||
"Division by zero at line 30, column 36">>,
|
||||
<<?Pos(32, 3)
|
||||
"The function unused_stateful is unnecessarily marked as stateful at line 32, column 3">>,
|
||||
<<?Pos(35, 31)
|
||||
"The variable unused_arg is defined at line 35, column 31 but never used">>,
|
||||
<<?Pos(36, 9)
|
||||
"The variable unused_var is defined at line 36, column 9 but never used">>,
|
||||
<<?Pos(41, 3)
|
||||
"The function unused_function is defined at line 41, column 3 but never used">>,
|
||||
<<?Pos(42, 3)
|
||||
"The function recursive_unused_function is defined at line 42, column 3 but never used">>,
|
||||
<<?Pos(43, 3)
|
||||
"The function called_unused_function1 is defined at line 43, column 3 but never used">>,
|
||||
<<?Pos(44, 3)
|
||||
"The function called_unused_function2 is defined at line 44, column 3 but never used">>,
|
||||
<<?Pos(48, 5)
|
||||
"Unused return value at line 48, column 5">>
|
||||
])
|
||||
].
|
||||
|
||||
-define(Path(File), "code_errors/" ??File).
|
||||
|
49
test/contracts/warnings.aes
Normal file
49
test/contracts/warnings.aes
Normal file
@ -0,0 +1,49 @@
|
||||
// Used include
|
||||
include "Pair.aes"
|
||||
// Unused include
|
||||
include "Triple.aes"
|
||||
|
||||
namespace UnusedNamespace =
|
||||
function f() = 1 + g()
|
||||
|
||||
// Used in f
|
||||
private function g() = 2
|
||||
|
||||
// Unused
|
||||
private function h() = 3
|
||||
|
||||
contract Warnings =
|
||||
|
||||
type state = int
|
||||
|
||||
type unused_type = bool
|
||||
|
||||
entrypoint init(p) = Pair.fst(p) + Pair.snd(p)
|
||||
|
||||
stateful entrypoint negative_spend(to : address) = Chain.spend(to, -1)
|
||||
|
||||
entrypoint shadowing() =
|
||||
let x = 1
|
||||
let x = 2
|
||||
x
|
||||
|
||||
entrypoint division_by_zero(x) = x / 0
|
||||
|
||||
stateful entrypoint unused_stateful() = 1
|
||||
stateful entrypoint used_stateful(x : int) = put(x)
|
||||
|
||||
entrypoint unused_variables(unused_arg : int) =
|
||||
let unused_var = 10
|
||||
let z = 20
|
||||
z
|
||||
|
||||
// Unused functions
|
||||
function unused_function() = ()
|
||||
function recursive_unused_function() = recursive_unused_function()
|
||||
function called_unused_function1() = called_unused_function2()
|
||||
function called_unused_function2() = called_unused_function1()
|
||||
|
||||
function rv() = 1
|
||||
entrypoint unused_return_value() =
|
||||
rv()
|
||||
2
|
Loading…
x
Reference in New Issue
Block a user