From 99bb3fe1fb4914de565eeabcce66dd6c7045205f Mon Sep 17 00:00:00 2001 From: Gaith Hallak Date: Tue, 21 Mar 2023 13:55:18 +0300 Subject: [PATCH 1/2] Mark only included files as potentially unused (#442) * Mark only included files as potentially unused * Update CHANGELOG * Add test --- CHANGELOG.md | 1 + src/aeso_ast_infer_types.erl | 11 ++++++++--- test/aeso_compiler_tests.erl | 1 + test/contracts/warning_unused_include_no_include.aes | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 test/contracts/warning_unused_include_no_include.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf97f5..ed83675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Removed ### Fixed +- Warning about unused include when there is no include. ## [7.1.0] ### Added diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 2184662..ac0ef42 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -3207,9 +3207,14 @@ when_warning(Warn, Do) -> %% 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}) + IsIncluded = aeso_syntax:get_ann(include_type, Ann, none) =/= none, + case IsIncluded of + false -> ok; + true -> + case aeso_syntax:get_ann(file, Ann, no_file) of + no_file -> ok; + File -> ets_insert(warnings, {unused_include, File, SrcFile}) + end end. used_include(Ann) -> diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index e070fd7..9eb002d 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -69,6 +69,7 @@ simple_compile_test_() -> [ {"Testing warning messages", fun() -> #{ warnings := Warnings } = compile("warnings", [warn_all]), + #{ warnings := [] } = compile("warning_unused_include_no_include", [warn_all]), check_warnings(warnings(), Warnings) end} ] ++ []. diff --git a/test/contracts/warning_unused_include_no_include.aes b/test/contracts/warning_unused_include_no_include.aes new file mode 100644 index 0000000..edf2fd2 --- /dev/null +++ b/test/contracts/warning_unused_include_no_include.aes @@ -0,0 +1,5 @@ +namespace N = + function nconst() = 1 + +main contract C = + entrypoint f() = N.nconst() From 7b6eba531925d8282697e9c22e4b28717379f311 Mon Sep 17 00:00:00 2001 From: Gaith Hallak Date: Wed, 12 Apr 2023 14:20:41 +0300 Subject: [PATCH 2/2] Introduce contract-level compile-time constants (#432) * Allow compile-time constants as toplevel declarations * Remove the test that fails on toplevel consts * Warn when shadowing a constant * Allow records to be used as compile time constants * Allow data constructors in compile-time constants * Disable some warnings for toplevel constants Since variables and functions cannot be used in the definition of a compile time constants, the following warnings are not going to be reported: * Used/Unused variable * Used/Unused function * Do not reverse constants declarations * Add tests for all valid expressions * Add test for accessing const from namespace * Revert "Do not reverse constants declarations" This reverts commit c4647fadacd134866e4be9c2ab4b0d54870a35fd. * Add test for assigining constant to a constant * Show empty map or record error when assigning to const * Report all invalid constant expressions before fail * Allow accessing records fields in toplevel consts * Undo a mistake * Add test for warning on const shadowing * Show error message when using pattern matching for consts * Remove unused error * Ban toplevel constants in contract interfaces * Varibles rename * Change the error message for invalid_const_id * Make constants public in namespaces and private in contracts * Add a warning about unused constants in contracts * Use ban_when_const for function applications * Test for qualified access of constants in functions * Add failing tests * Add test for the unused const warning * Update CHANGELOG * Update all_syntax test file * Treat expr and type inside bound as bound * Allow typed ids to be used for constants * List valid exprs in the error message for invalid exprs * Fix tests * Update the docs about constants * Update syntax docs * Check validity of const exprs in a separate functions * Call both resolve_const and resolve_fun from resolve_var --- CHANGELOG.md | 7 + docs/sophia_features.md | 26 ++ docs/sophia_syntax.md | 1 + src/aeso_ast_infer_types.erl | 263 ++++++++++++++++-- src/aeso_ast_to_fcode.erl | 28 +- src/aeso_syntax_utils.erl | 7 +- test/aeso_compiler_tests.erl | 85 +++++- test/contracts/all_syntax.aes | 3 + test/contracts/toplevel_constants.aes | 64 +++++ ...plevel_constants_contract_as_namespace.aes | 11 + test/contracts/toplevel_constants_cycles.aes | 6 + .../toplevel_constants_in_interface.aes | 7 + .../toplevel_constants_invalid_expr.aes | 21 ++ .../toplevel_constants_invalid_id.aes | 3 + test/contracts/toplevel_let.aes | 3 - test/contracts/warnings.aes | 21 ++ 16 files changed, 511 insertions(+), 45 deletions(-) create mode 100644 test/contracts/toplevel_constants.aes create mode 100644 test/contracts/toplevel_constants_contract_as_namespace.aes create mode 100644 test/contracts/toplevel_constants_cycles.aes create mode 100644 test/contracts/toplevel_constants_in_interface.aes create mode 100644 test/contracts/toplevel_constants_invalid_expr.aes create mode 100644 test/contracts/toplevel_constants_invalid_id.aes delete mode 100644 test/contracts/toplevel_let.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index ed83675..66a4b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Toplevel compile-time constants + ``` + namespace N = + let nc = 1 + contract C = + let cc = 2 + ``` ### Changed ### Removed ### Fixed diff --git a/docs/sophia_features.md b/docs/sophia_features.md index 7ff4f0e..e8170c0 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -573,6 +573,32 @@ contract C = A hole expression found in the example above will generate the error `` Found a hole of type `(int) => int` ``. This says that the compiler expects a function from `int` to `int` in place of the `???` placeholder. +## Constants + +Constants in Sophia are contract-level bindings that can be used in either contracts or namespaces. The value of a constant can be a literal, another constant, or arithmetic operations applied to other constants. Lists, tuples, maps, and records can also be used to define a constant as long as their elements are also constants. + +The following visibility rules apply to constants: +* Constants defined inside a contract are private in that contract. Thus, cannot be accessed through instances of their defining contract. +* Constants defined inside a namespace are public. Thus, can be used in other contracts or namespaces. +* Constants cannot be defined inside a contract interface. + +When a constant is shadowed, it can be accessed using its qualified name: + +``` +contract C = + let c = 1 + entrypoint f() = + let c = 2 + c + C.c // the result is 3 +``` + +The name of the constant must be an id; therefore, no pattern matching is allowed when defining a constant: + +``` +contract C + let x::y::_ = [1,2,3] // this will result in an error +``` + ## Arithmetic Sophia integers (`int`) are represented by arbitrary-sized signed words and support the following diff --git a/docs/sophia_syntax.md b/docs/sophia_syntax.md index e57cc84..f0df9e6 100644 --- a/docs/sophia_syntax.md +++ b/docs/sophia_syntax.md @@ -104,6 +104,7 @@ Implement ::= ':' Sep1(Con, ',') Decl ::= 'type' Id ['(' TVar* ')'] '=' TypeAlias | 'record' Id ['(' TVar* ')'] '=' RecordType | 'datatype' Id ['(' TVar* ')'] '=' DataType + | 'let' Id [':' Type] '=' Expr | (EModifier* 'entrypoint' | FModifier* 'function') Block(FunDecl) | Using diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index ac0ef42..9514dea 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -124,15 +124,18 @@ -type variance() :: invariant | covariant | contravariant | bivariant. --type fun_info() :: {aeso_syntax:ann(), typesig() | type()}. --type type_info() :: {aeso_syntax:ann(), typedef()}. --type var_info() :: {aeso_syntax:ann(), utype()}. +-type fun_info() :: {aeso_syntax:ann(), typesig() | type()}. +-type type_info() :: {aeso_syntax:ann(), typedef()}. +-type const_info() :: {aeso_syntax:ann(), type()}. +-type var_info() :: {aeso_syntax:ann(), utype()}. --type fun_env() :: [{name(), fun_info()}]. --type type_env() :: [{name(), type_info()}]. +-type fun_env() :: [{name(), fun_info()}]. +-type type_env() :: [{name(), type_info()}]. +-type const_env() :: [{name(), const_info()}]. -record(scope, { funs = [] :: fun_env() , types = [] :: type_env() + , consts = [] :: const_env() , access = public :: access() , kind = namespace :: namespace | contract , ann = [{origin, system}] :: aeso_syntax:ann() @@ -152,6 +155,7 @@ , in_guard = false :: boolean() , stateful = false :: boolean() , unify_throws = true :: boolean() + , current_const = none :: none | aeso_syntax:id() , current_function = none :: none | aeso_syntax:id() , what = top :: top | namespace | contract | contract_interface }). @@ -183,9 +187,13 @@ pop_scope(Env) -> get_scope(#env{ scopes = Scopes }, Name) -> maps:get(Name, Scopes, false). +-spec get_current_scope(env()) -> scope(). +get_current_scope(#env{ namespace = NS, scopes = Scopes }) -> + maps:get(NS, Scopes). + -spec on_current_scope(env(), fun((scope()) -> scope())) -> env(). on_current_scope(Env = #env{ namespace = NS, scopes = Scopes }, Fun) -> - Scope = maps:get(NS, Scopes), + Scope = get_current_scope(Env), Env#env{ scopes = Scopes#{ NS => Fun(Scope) } }. -spec on_scopes(env(), fun((scope()) -> scope())) -> env(). @@ -193,8 +201,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 = #env{ vars = Vars }) -> - when_warning(warn_shadowing, fun() -> warn_potential_shadowing(Ann, X, Vars) end), +bind_var({id, Ann, X}, T, Env) -> + when_warning(warn_shadowing, fun() -> warn_potential_shadowing(Env, Ann, X) end), Env#env{ vars = [{X, {Ann, T}} | Env#env.vars] }. -spec bind_vars([{aeso_syntax:id(), utype()}], env()) -> env(). @@ -247,6 +255,37 @@ bind_type(X, Ann, Def, Env) -> Scope#scope{ types = [{X, {Ann, Def}} | Types] } end). +-spec bind_const(name(), aeso_syntax:ann(), type(), env()) -> env(). +bind_const(X, Ann, Type, Env) -> + case lookup_env(Env, term, Ann, [X]) of + false -> + on_current_scope(Env, fun(Scope = #scope{ consts = Consts }) -> + Scope#scope{ consts = [{X, {Ann, Type}} | Consts] } + end); + _ -> + type_error({duplicate_definition, X, [Ann, aeso_syntax:get_ann(Type)]}), + Env + end. + +-spec bind_consts(env(), #{ name() => aeso_syntax:decl() }, [{acyclic, name()} | {cyclic, [name()]}], [aeso_syntax:decl()]) -> + {env(), [aeso_syntax:decl()]}. +bind_consts(Env, _Consts, [], Acc) -> + {Env, lists:reverse(Acc)}; +bind_consts(Env, Consts, [{cyclic, Xs} | _SCCs], _Acc) -> + ConstDecls = [ maps:get(X, Consts) || X <- Xs ], + type_error({mutually_recursive_constants, lists:reverse(ConstDecls)}), + {Env, []}; +bind_consts(Env, Consts, [{acyclic, X} | SCCs], Acc) -> + case maps:get(X, Consts, undefined) of + Const = {letval, Ann, Id, _} -> + NewConst = {letval, _, {typed, _, _, Type}, _} = infer_const(Env, Const), + NewEnv = bind_const(name(Id), Ann, Type, Env), + bind_consts(NewEnv, Consts, SCCs, [NewConst | Acc]); + undefined -> + %% When a used id is not a letval, a type error will be thrown + bind_consts(Env, Consts, SCCs, Acc) + end. + %% Bind state primitives -spec bind_state(env()) -> env(). bind_state(Env) -> @@ -431,21 +470,30 @@ lookup_env1(#env{ namespace = Current, used_namespaces = UsedNamespaces, scopes %% Get the scope case maps:get(Qual, Scopes, false) of false -> false; %% TODO: return reason for not in scope - #scope{ funs = Funs, types = Types } -> + #scope{ funs = Funs, types = Types, consts = Consts, kind = ScopeKind } -> Defs = case Kind of type -> Types; term -> Funs end, %% Look up the unqualified name case proplists:get_value(Name, Defs, false) of - false -> false; + false -> + case proplists:get_value(Name, Consts, false) of + false -> + false; + Const when AllowPrivate; ScopeKind == namespace -> + {QName, Const}; + Const -> + type_error({contract_treated_as_namespace_constant, Ann, QName}), + {QName, Const} + end; {reserved_init, Ann1, Type} -> type_error({cannot_call_init_function, Ann}), {QName, {Ann1, Type}}; %% Return the type to avoid an extra not-in-scope error {contract_fun, Ann1, Type} when AllowPrivate orelse QNameIsEvent -> {QName, {Ann1, Type}}; {contract_fun, Ann1, Type} -> - type_error({contract_treated_as_namespace, Ann, QName}), + type_error({contract_treated_as_namespace_entrypoint, Ann, QName}), {QName, {Ann1, Type}}; {Ann1, _} = E -> %% Check that it's not private (or we can see private funs) @@ -486,8 +534,11 @@ qname({qid, _, Xs}) -> Xs; qname({con, _, X}) -> [X]; qname({qcon, _, Xs}) -> Xs. --spec name(aeso_syntax:id() | aeso_syntax:con()) -> name(). -name({_, _, X}) -> X. +-spec name(Named | {typed, _, Named, _}) -> name() when + Named :: aeso_syntax:id() | aeso_syntax:con(). +name({typed, _, X, _}) -> name(X); +name({id, _, X}) -> X; +name({con, _, X}) -> X. -spec qid(aeso_syntax:ann(), qname()) -> aeso_syntax:id() | aeso_syntax:qid(). qid(Ann, [X]) -> {id, Ann, X}; @@ -1054,6 +1105,7 @@ infer_contract(Env0, What, Defs0, Options) -> ({fun_clauses, _, _, _, _}) -> function; ({fun_decl, _, _, _}) -> prototype; ({using, _, _, _, _}) -> using; + ({letval, _, _, _}) -> constant; (_) -> unexpected end, Get = fun(K, In) -> [ Def || Def <- In, Kind(Def) == K ] end, @@ -1069,11 +1121,12 @@ infer_contract(Env0, What, Defs0, Options) -> contract_interface -> Env1; contract -> bind_state(Env1) %% bind state and put end, - {ProtoSigs, Decls} = lists:unzip([ check_fundecl(Env1, Decl) || Decl <- Get(prototype, Defs) ]), + {Env2C, Consts} = check_constants(Env2, Get(constant, Defs)), + {ProtoSigs, Decls} = lists:unzip([ check_fundecl(Env2C, Decl) || Decl <- Get(prototype, Defs) ]), [ type_error({missing_definition, Id}) || {fun_decl, _, Id, _} <- Decls, What =:= contract, get_option(no_code, false) =:= false ], - Env3 = bind_funs(ProtoSigs, Env2), + Env3 = bind_funs(ProtoSigs, Env2C), Functions = Get(function, Defs), %% Check for duplicates in Functions (we turn it into a map below) FunBind = fun({letfun, Ann, {id, _, Fun}, _, _, _}) -> {Fun, {tuple_t, Ann, []}}; @@ -1093,7 +1146,7 @@ infer_contract(Env0, What, Defs0, Options) -> check_entrypoints(Defs1), destroy_and_report_type_errors(Env4), %% Add inferred types of definitions - {Env5, TypeDefs ++ Decls ++ Defs1}. + {Env5, TypeDefs ++ Decls ++ Consts ++ Defs1}. %% Restructure blocks into multi-clause fundefs (`fun_clauses`). -spec process_blocks([aeso_syntax:decl()]) -> [aeso_syntax:decl()]. @@ -1243,6 +1296,21 @@ opposite_variance(covariant) -> contravariant; opposite_variance(contravariant) -> covariant; opposite_variance(bivariant) -> bivariant. +-spec check_constants(env(), [aeso_syntax:decl()]) -> {env(), [aeso_syntax:decl()]}. +check_constants(Env = #env{ what = What }, Consts) -> + HasValidId = fun({letval, _, {id, _, _}, _}) -> true; + ({letval, _, {typed, _, {id, _, _}, _}, _}) -> true; + (_) -> false + end, + {Valid, Invalid} = lists:partition(HasValidId, Consts), + [ type_error({invalid_const_id, aeso_syntax:get_ann(Pat)}) || {letval, _, Pat, _} <- Invalid ], + [ type_error({illegal_const_in_interface, Ann}) || {letval, Ann, _, _} <- Valid, What == contract_interface ], + when_warning(warn_unused_constants, fun() -> potential_unused_constants(Env, Valid) end), + ConstMap = maps:from_list([ {name(Id), Const} || Const = {letval, _, Id, _} <- Valid ]), + DepGraph = maps:map(fun(_, Const) -> aeso_syntax_utils:used_ids(Const) end, ConstMap), + SCCs = aeso_utils:scc(DepGraph), + bind_consts(Env, ConstMap, SCCs, []). + check_usings(Env, []) -> Env; check_usings(Env = #env{ used_namespaces = UsedNamespaces }, [{using, Ann, Con, Alias, Parts} | Rest]) -> @@ -1687,9 +1755,19 @@ lookup_name(Env = #env{ namespace = NS, current_function = CurFn }, As, Id, Opti type_error({unbound_variable, Id}), {Id, fresh_uvar(As)}; {QId, {_, Ty}} -> - when_warning(warn_unused_variables, fun() -> used_variable(NS, name(CurFn), QId) end), - when_warning(warn_unused_functions, - fun() -> register_function_call(NS ++ qname(CurFn), QId) end), + %% Variables and functions cannot be used when CurFn is `none`. + %% i.e. they cannot be used in toplevel constants + [ begin + when_warning( + warn_unused_variables, + fun() -> used_variable(NS, name(CurFn), QId) end), + when_warning( + warn_unused_functions, + fun() -> register_function_call(NS ++ qname(CurFn), QId) end) + end || CurFn =/= none ], + + when_warning(warn_unused_constants, fun() -> used_constant(NS, QId) end), + Freshen = proplists:get_value(freshen, Options, false), check_stateful(Env, Id, Ty), Ty1 = case Ty of @@ -2054,6 +2132,81 @@ infer_expr(Env, Let = {letfun, Attrs, _, _, _, _}) -> type_error({missing_body_for_let, Attrs}), infer_expr(Env, {block, Attrs, [Let, abort_expr(Attrs, "missing body")]}). +check_valid_const_expr({bool, _, _}) -> + true; +check_valid_const_expr({int, _, _}) -> + true; +check_valid_const_expr({char, _, _}) -> + true; +check_valid_const_expr({string, _, _}) -> + true; +check_valid_const_expr({bytes, _, _}) -> + true; +check_valid_const_expr({account_pubkey, _, _}) -> + true; +check_valid_const_expr({oracle_pubkey, _, _}) -> + true; +check_valid_const_expr({oracle_query_id, _, _}) -> + true; +check_valid_const_expr({contract_pubkey, _, _}) -> + true; +check_valid_const_expr({id, _, "_"}) -> + true; +check_valid_const_expr({Tag, _, _}) when Tag == id; Tag == qid; Tag == con; Tag == qcon -> + true; +check_valid_const_expr({tuple, _, Cpts}) -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(C) || C <- Cpts ]); +check_valid_const_expr({list, _, Elems}) -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(Elem) || Elem <- Elems ]); +check_valid_const_expr({list_comp, _, _, _}) -> + false; +check_valid_const_expr({typed, _, Body, _}) -> + check_valid_const_expr(Body); +check_valid_const_expr({app, Ann, Fun, Args0}) -> + {_, Args} = split_args(Args0), + case aeso_syntax:get_ann(format, Ann) of + infix -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(Arg) || Arg <- Args ]); + prefix -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(Arg) || Arg <- Args ]); + _ -> + %% Applications of data constructors are allowed in constants + lists:member(element(1, Fun), [con, qcon]) + end; +check_valid_const_expr({'if', _, _, _, _}) -> + false; +check_valid_const_expr({switch, _, _, _}) -> + false; +check_valid_const_expr({record, _, Fields}) -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(Expr) || {field, _, _, Expr} <- Fields ]); +check_valid_const_expr({record, _, _, _}) -> + false; +check_valid_const_expr({proj, _, Record, _}) -> + check_valid_const_expr(Record); +% Maps +check_valid_const_expr({map_get, _, _, _}) -> %% map lookup + false; +check_valid_const_expr({map_get, _, _, _, _}) -> %% map lookup with default + false; +check_valid_const_expr({map, _, KVs}) -> %% map construction + lists:all(fun(X) -> X end, [ check_valid_const_expr(K) andalso check_valid_const_expr(V) || {K, V} <- KVs ]); +check_valid_const_expr({map, _, _, _}) -> %% map update + false; +check_valid_const_expr({block, _, _}) -> + false; +check_valid_const_expr({record_or_map_error, _, Fields}) -> + lists:all(fun(X) -> X end, [ check_valid_const_expr(Expr) || {field, _, _, Expr} <- Fields ]); +check_valid_const_expr({record_or_map_error, _, _, _}) -> + false; +check_valid_const_expr({lam, _, _, _}) -> + false; +check_valid_const_expr({letpat, _, _, _}) -> + false; +check_valid_const_expr({letval, _, _, _}) -> + false; +check_valid_const_expr({letfun, _, _, _, _, _}) -> + false. + infer_var_args_fun(Env, {typed, Ann, Fun, FunType0}, NamedArgs, ArgTypes) -> FunType = case Fun of @@ -2178,9 +2331,14 @@ infer_pattern(Env, Pattern) -> NewPattern = infer_expr(NewEnv, Pattern), {NewEnv#env{ in_pattern = Env#env.in_pattern }, NewPattern}. -infer_case(Env = #env{ namespace = NS, current_function = {id, _, Fun} }, Attrs, Pattern, ExprType, GuardedBranches, SwitchType) -> +infer_case(Env = #env{ namespace = NS, current_function = FunId }, 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), + + %% Make sure we are inside a function before warning about potentially unused var + [ when_warning(warn_unused_variables, + fun() -> potential_unused_variables(NS, Fun, free_vars(Pattern)) end) + || {id, _, Fun} <- [FunId] ], + InferGuardedBranches = fun({guarded, Ann, Guards, Branch}) -> NewGuards = lists:map(fun(Guard) -> check_expr(NewEnv#env{ in_guard = true }, Guard, {id, Attrs, "bool"}) @@ -2214,6 +2372,19 @@ infer_block(Env, Attrs, [E|Rest], BlockType) -> when_warning(warn_unused_return_value, fun() -> potential_unused_return_value(NewE) end), [NewE|infer_block(Env, Attrs, Rest, BlockType)]. +infer_const(Env, {letval, Ann, TypedId = {typed, _, Id = {id, _, _}, Type}, Expr}) -> + check_valid_const_expr(Expr) orelse type_error({invalid_const_expr, Id}), + NewExpr = check_expr(Env#env{ current_const = Id }, Expr, Type), + {letval, Ann, TypedId, NewExpr}; +infer_const(Env, {letval, Ann, Id = {id, AnnId, _}, Expr}) -> + check_valid_const_expr(Expr) orelse type_error({invalid_const_expr, Id}), + create_constraints(), + NewExpr = {typed, _, _, Type} = infer_expr(Env#env{ current_const = Id }, Expr), + solve_then_destroy_and_report_unsolved_constraints(Env), + IdType = setelement(2, Type, AnnId), + NewId = {typed, aeso_syntax:get_ann(Id), Id, IdType}, + instantiate({letval, Ann, NewId, NewExpr}). + infer_infix({BoolOp, As}) when BoolOp =:= '&&'; BoolOp =:= '||' -> Bool = {id, As, "bool"}, @@ -3177,6 +3348,7 @@ all_warnings() -> [ warn_unused_includes , warn_unused_stateful , warn_unused_variables + , warn_unused_constants , warn_unused_typedefs , warn_unused_return_value , warn_unused_functions @@ -3254,6 +3426,17 @@ used_variable(Namespace, Fun, [VarName]) -> ets_match_delete(warnings, {unused_variable, '_', Namespace, Fun, VarName}); used_variable(_, _, _) -> ok. +%% Warnings (Unused constants) + +potential_unused_constants(#env{ what = namespace }, _Consts) -> + []; +potential_unused_constants(#env{ namespace = Namespace }, Consts) -> + [ ets_insert(warnings, {unused_constant, Ann, Namespace, Name}) || {letval, _, {id, Ann, Name}, _} <- Consts ]. + +used_constant(Namespace = [Contract], [Contract, ConstName]) -> + ets_match_delete(warnings, {unused_constant, '_', Namespace, ConstName}); +used_constant(_, _) -> ok. + %% Warnings (Unused return value) potential_unused_return_value({typed, Ann, {app, _, {typed, _, _, {fun_t, _, _, _, {id, _, Type}}}, _}, _}) when Type /= "unit" -> @@ -3299,9 +3482,11 @@ destroy_and_report_unused_functions() -> %% Warnings (Shadowing) -warn_potential_shadowing(_, "_", _) -> ok; -warn_potential_shadowing(Ann, Name, Vars) -> - case proplists:get_value(Name, Vars, false) of +warn_potential_shadowing(_, _, "_") -> ok; +warn_potential_shadowing(Env = #env{ vars = Vars }, Ann, Name) -> + CurrentScope = get_current_scope(Env), + Consts = CurrentScope#scope.consts, + case proplists:get_value(Name, Vars ++ Consts, false) of false -> ok; {AnnOld, _} -> ets_insert(warnings, {shadowing, Ann, Name, AnnOld}) end. @@ -3543,10 +3728,6 @@ mk_error({type_decl, _, {id, Pos, Name}, _}) -> Msg = io_lib:format("Empty type declarations are not supported. Type `~s` lacks a definition", [Name]), mk_t_err(pos(Pos), Msg); -mk_error({letval, _Pos, {id, Pos, Name}, _Def}) -> - Msg = io_lib:format("Toplevel \"let\" definitions are not supported. Value `~s` could be replaced by 0-argument function.", - [Name]), - mk_t_err(pos(Pos), Msg); mk_error({stateful_not_allowed, Id, Fun}) -> Msg = io_lib:format("Cannot reference stateful function `~s` in the definition of non-stateful function `~s`.", [pp(Id), pp(Fun)]), @@ -3630,10 +3811,14 @@ mk_error({cannot_call_init_function, Ann}) -> Msg = "The 'init' function is called exclusively by the create contract transaction " "and cannot be called from the contract code.", mk_t_err(pos(Ann), Msg); -mk_error({contract_treated_as_namespace, Ann, [Con, Fun] = QName}) -> +mk_error({contract_treated_as_namespace_entrypoint, Ann, [Con, Fun] = QName}) -> Msg = io_lib:format("Invalid call to contract entrypoint `~s`.", [string:join(QName, ".")]), Cxt = io_lib:format("It must be called as `c.~s` for some `c : ~s`.", [Fun, Con]), mk_t_err(pos(Ann), Msg, Cxt); +mk_error({contract_treated_as_namespace_constant, Ann, QName}) -> + Msg = io_lib:format("Invalid use of the contract constant `~s`.", [string:join(QName, ".")]), + Cxt = "Toplevel contract constants can only be used in the contracts where they are defined.", + mk_t_err(pos(Ann), Msg, Cxt); mk_error({bad_top_level_decl, Decl}) -> What = case element(1, Decl) of letval -> "function or entrypoint"; @@ -3794,6 +3979,23 @@ mk_error({unpreserved_payablity, Kind, ContractCon, InterfaceCon}) -> Msg = io_lib:format("Non-payable ~s `~s` cannot implement payable interface `~s`", [KindStr, name(ContractCon), name(InterfaceCon)]), mk_t_err(pos(ContractCon), Msg); +mk_error({mutually_recursive_constants, Consts}) -> + Msg = [ "Mutual recursion detected between the constants", + [ io_lib:format("\n - `~s` at ~s", [name(Id), pp_loc(Ann)]) + || {letval, Ann, Id, _} <- Consts ] ], + [{letval, Ann, _, _} | _] = Consts, + mk_t_err(pos(Ann), Msg); +mk_error({invalid_const_id, Ann}) -> + Msg = "The name of the compile-time constant cannot have pattern matching", + mk_t_err(pos(Ann), Msg); +mk_error({invalid_const_expr, ConstId}) -> + Msg = io_lib:format("Invalid expression in the definition of the constant `~s`", [name(ConstId)]), + Cxt = "You can only use the following expressions as constants: " + "literals, lists, tuples, maps, and other constants", + mk_t_err(pos(aeso_syntax:get_ann(ConstId)), Msg, Cxt); +mk_error({illegal_const_in_interface, Ann}) -> + Msg = "Cannot define toplevel constants inside a contract interface", + mk_t_err(pos(Ann), Msg); mk_error(Err) -> Msg = io_lib:format("Unknown error: ~p", [Err]), mk_t_err(pos(0, 0), Msg). @@ -3807,6 +4009,9 @@ mk_warning({unused_stateful, Ann, FunName}) -> mk_warning({unused_variable, Ann, _Namespace, _Fun, VarName}) -> Msg = io_lib:format("The variable `~s` is defined but never used.", [VarName]), aeso_warnings:new(pos(Ann), Msg); +mk_warning({unused_constant, Ann, _Namespace, ConstName}) -> + Msg = io_lib:format("The constant `~s` is defined but never used.", [ConstName]), + aeso_warnings:new(pos(Ann), Msg); mk_warning({unused_typedef, Ann, QName, _Arity}) -> Msg = io_lib:format("The type `~s` is defined but never used.", [lists:last(QName)]), aeso_warnings:new(pos(Ann), Msg); diff --git a/src/aeso_ast_to_fcode.erl b/src/aeso_ast_to_fcode.erl index f3dd9cc..c83d01a 100644 --- a/src/aeso_ast_to_fcode.erl +++ b/src/aeso_ast_to_fcode.erl @@ -160,6 +160,7 @@ context => context(), vars => [var_name()], functions := #{ fun_name() => fun_def() }, + consts := #{ var_name() => fexpr() }, saved_fresh_names => #{ var_name() => var_name() } }. @@ -240,7 +241,8 @@ init_env(Options) -> ["Chain", "GAAttachTx"] => #con_tag{ tag = 21, arities = ChainTxArities } }, options => Options, - functions => #{} + functions => #{}, + consts => #{} }. -spec builtins() -> builtins(). @@ -395,7 +397,11 @@ decl_to_fcode(Env = #{ functions := Funs }, {letfun, Ann, {id, _, Name}, Args, R return => FRet, body => FBody }, NewFuns = Funs#{ FName => Def }, - Env#{ functions := NewFuns }. + Env#{ functions := NewFuns }; +decl_to_fcode(Env = #{ consts := Consts }, {letval, _, {typed, _, {id, _, X}, _}, Val}) -> + FVal = expr_to_fcode(Env, Val), + NewConsts = Consts#{ qname(Env, X) => FVal }, + Env#{ consts := NewConsts }. -spec typedef_to_fcode(env(), aeso_syntax:id(), [aeso_syntax:tvar()], aeso_syntax:typedef()) -> env(). typedef_to_fcode(Env, {id, _, Name}, Xs, Def) -> @@ -1722,9 +1728,23 @@ bind_var(Env = #{ vars := Vars }, X) -> Env#{ vars := [X | Vars] }. resolve_var(#{ vars := Vars } = Env, [X]) -> case lists:member(X, Vars) of true -> {var, X}; - false -> resolve_fun(Env, [X]) + false -> + case resolve_const(Env, [X]) of + false -> resolve_fun(Env, [X]); + Const -> Const + end end; -resolve_var(Env, Q) -> resolve_fun(Env, Q). +resolve_var(Env, Q) -> + case resolve_const(Env, Q) of + false -> resolve_fun(Env, Q); + Const -> Const + end. + +resolve_const(#{ consts := Consts }, Q) -> + case maps:get(Q, Consts, not_found) of + not_found -> false; + Val -> Val + end. resolve_fun(#{ fun_env := Funs, builtins := Builtin } = Env, Q) -> case {maps:get(Q, Funs, not_found), maps:get(Q, Builtin, not_found)} of diff --git a/src/aeso_syntax_utils.erl b/src/aeso_syntax_utils.erl index e50f2d4..c9ee23a 100644 --- a/src/aeso_syntax_utils.erl +++ b/src/aeso_syntax_utils.erl @@ -31,11 +31,13 @@ | aeso_syntax:field(aeso_syntax:expr()) | aeso_syntax:stmt(). fold(Alg = #alg{zero = Zero, plus = Plus, scoped = Scoped}, Fun, K, X) -> + ExprKind = if K == bind_expr -> bind_expr; true -> expr end, + TypeKind = if K == bind_type -> bind_type; true -> type end, Sum = fun(Xs) -> lists:foldl(Plus, Zero, Xs) end, Same = fun(A) -> fold(Alg, Fun, K, A) end, Decl = fun(D) -> fold(Alg, Fun, decl, D) end, - Type = fun(T) -> fold(Alg, Fun, type, T) end, - Expr = fun(E) -> fold(Alg, Fun, expr, E) end, + Type = fun(T) -> fold(Alg, Fun, TypeKind, T) end, + Expr = fun(E) -> fold(Alg, Fun, ExprKind, E) end, BindExpr = fun(P) -> fold(Alg, Fun, bind_expr, P) end, BindType = fun(T) -> fold(Alg, Fun, bind_type, T) end, Top = Fun(K, X), @@ -155,4 +157,3 @@ used(D) -> (_, _) -> #{} end, decl, D)), lists:filter(NotBound, Xs). - diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 9eb002d..ed50821 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -222,6 +222,7 @@ compilable_contracts() -> "unapplied_contract_call", "unapplied_named_arg_builtin", "resolve_field_constraint_by_arity", + "toplevel_constants", "test" % Custom general-purpose test file. Keep it last on the list. ]. @@ -286,7 +287,11 @@ warnings() -> <>, <> + "The function `dec` is defined but never used.">>, + <>, + <> ]). failing_contracts() -> @@ -658,10 +663,6 @@ failing_contracts() -> [<>]) - , ?TYPE_ERROR(toplevel_let, - [<>]) , ?TYPE_ERROR(empty_typedecl, [< <>, <> + "The function `dec` is defined but never used.">>, + <>, + <> ]) , ?TYPE_ERROR(polymorphism_contract_interface_recursive, [< <> ]) + , ?TYPE_ERROR(toplevel_constants_contract_as_namespace, + [<>, + <>, + <>, + <>, + <> + ]) + , ?TYPE_ERROR(toplevel_constants_cycles, + [<>, + <> + ]) + , ?TYPE_ERROR(toplevel_constants_in_interface, + [<>, + <>, + <> + ]) + , ?TYPE_ERROR(toplevel_constants_invalid_expr, + [<>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <> + ]) + , ?TYPE_ERROR(toplevel_constants_invalid_id, + [<>, + <> + ]) ]. validation_test_() -> diff --git a/test/contracts/all_syntax.aes b/test/contracts/all_syntax.aes index 513f3be..823de77 100644 --- a/test/contracts/all_syntax.aes +++ b/test/contracts/all_syntax.aes @@ -6,6 +6,7 @@ namespace Ns = datatype d('a) = D | S(int) | M('a, list('a), int) private function fff() = 123 + let const = 1 stateful entrypoint f (1, x) = (_) => x @@ -33,6 +34,8 @@ contract AllSyntax = type state = shakespeare(int) + let cc = "str" + entrypoint init() = { johann = 1000, wolfgang = -10, diff --git a/test/contracts/toplevel_constants.aes b/test/contracts/toplevel_constants.aes new file mode 100644 index 0000000..86ac9be --- /dev/null +++ b/test/contracts/toplevel_constants.aes @@ -0,0 +1,64 @@ +namespace N0 = + let nsconst = 1 + +namespace N = + let nsconst = N0.nsconst + +contract C = + datatype event = EventX(int, string) + + record account = { name : string, + balance : int } + + let c01 = 2425 + let c02 = -5 + let c03 = ak_2gx9MEFxKvY9vMG5YnqnXWv1hCsX7rgnfvBLJS4aQurustR1rt + let c04 = true + let c05 = Bits.none + let c06 = #fedcba9876543210 + let c07 = "str" + let c08 = [1, 2, 3] + let c09 = [(true, 24), (false, 19), (false, -42)] + let c10 = (42, "Foo", true) + let c11 = { name = "str", balance = 100000000 } + let c12 = {["foo"] = 19, ["bar"] = 42} + let c13 = Some(42) + let c14 = 11 : int + let c15 = EventX(0, "Hello") + let c16 = #000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f + let c17 = #000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f + let c18 = RelativeTTL(50) + let c19 = ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5 + let c20 = oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY + let c21 = ct_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ : C + let c22 = N.nsconst + let c23 = c01 + let c24 = c11.name + let c25 : int = 1 + + entrypoint f01() = c01 + entrypoint f02() = c02 + entrypoint f03() = c03 + entrypoint f04() = c04 + entrypoint f05() = c05 + entrypoint f06() = c06 + entrypoint f07() = c07 + entrypoint f08() = c08 + entrypoint f09() = c09 + entrypoint f10() = c10 + entrypoint f11() = c11 + entrypoint f12() = c12 + entrypoint f13() = c13 + entrypoint f14() = c14 + entrypoint f15() = c15 + entrypoint f16() = c16 + entrypoint f17() = c17 + entrypoint f18() = c18 + entrypoint f19() = c19 + entrypoint f20() = c20 + entrypoint f21() = c21 + entrypoint f22() = c22 + entrypoint f23() = c23 + entrypoint f24() = c24 + entrypoint f25() = c25 + entrypoint fqual() = C.c01 diff --git a/test/contracts/toplevel_constants_contract_as_namespace.aes b/test/contracts/toplevel_constants_contract_as_namespace.aes new file mode 100644 index 0000000..8bd6432 --- /dev/null +++ b/test/contracts/toplevel_constants_contract_as_namespace.aes @@ -0,0 +1,11 @@ +contract G = + let const = 1 + +main contract C = + let c = G.const + + stateful entrypoint f() = + let g = Chain.create() : G + + g.const + g.const() diff --git a/test/contracts/toplevel_constants_cycles.aes b/test/contracts/toplevel_constants_cycles.aes new file mode 100644 index 0000000..3096125 --- /dev/null +++ b/test/contracts/toplevel_constants_cycles.aes @@ -0,0 +1,6 @@ +contract C = + let selfcycle = selfcycle + + let cycle1 = cycle2 + let cycle2 = cycle3 + let cycle3 = cycle1 diff --git a/test/contracts/toplevel_constants_in_interface.aes b/test/contracts/toplevel_constants_in_interface.aes new file mode 100644 index 0000000..56558a7 --- /dev/null +++ b/test/contracts/toplevel_constants_in_interface.aes @@ -0,0 +1,7 @@ +contract interface I = + let (x::y::_) = [1,2,3] + let c = 10 + let d = 10 + +contract C = + entrypoint init() = () diff --git a/test/contracts/toplevel_constants_invalid_expr.aes b/test/contracts/toplevel_constants_invalid_expr.aes new file mode 100644 index 0000000..c9deecd --- /dev/null +++ b/test/contracts/toplevel_constants_invalid_expr.aes @@ -0,0 +1,21 @@ +main contract C = + record account = { name : string, + balance : int } + + let one = 1 + let opt = Some(5) + let acc = { name = "str", balance = 100000 } + let mpp = {["foo"] = 19, ["bar"] = 42} + + let c01 = [x | x <- [1,2,3,4,5]] + let c02 = [x + k | x <- [1,2,3,4,5], let k = x*x] + let c03 = [x + y | x <- [1,2,3,4,5], let k = x*x, if (k > 5), y <- [k, k+1, k+2]] + let c04 = if (one > 2) 3 else 4 + let c05 = switch (opt) + Some(x) => x + None => 2 + let c07 = acc{ balance = one } + let c08 = mpp["foo"] + let c09 = mpp["non" = 10] + let c10 = mpp{["foo"] = 20} + let c11 = (x) => x + 1 diff --git a/test/contracts/toplevel_constants_invalid_id.aes b/test/contracts/toplevel_constants_invalid_id.aes new file mode 100644 index 0000000..3b1820b --- /dev/null +++ b/test/contracts/toplevel_constants_invalid_id.aes @@ -0,0 +1,3 @@ +contract C = + let x::_ = [1,2,3,4] + let y::(p = z::_) = [1,2,3,4] diff --git a/test/contracts/toplevel_let.aes b/test/contracts/toplevel_let.aes deleted file mode 100644 index adca04c..0000000 --- a/test/contracts/toplevel_let.aes +++ /dev/null @@ -1,3 +0,0 @@ -contract C = - let this_is_illegal = 2/0 - entrypoint this_is_legal() = 2/0 \ No newline at end of file diff --git a/test/contracts/warnings.aes b/test/contracts/warnings.aes index 580d1d7..45e763d 100644 --- a/test/contracts/warnings.aes +++ b/test/contracts/warnings.aes @@ -65,3 +65,24 @@ contract Remote = contract C = payable stateful entrypoint call_missing_con() : int = (ct_1111111111111111111111111111112JF6Dz72 : Remote).id(value = 1, 0) + +namespace ShadowingConst = + let const = 1 + + function f() = + let const = 2 + const + +namespace UnusedConstNamespace = + // No warnings should be shown even though const is not used + let const = 1 + +contract UnusedConstContract = + // Only `c` should show a warning because it is never used in the contract + let a = 1 + let b = 2 + let c = 3 + + entrypoint f() = + // Both normal access and qualified access should prevent the unused const warning + a + UnusedConstContract.b