From a7b7aafced47a59c727137e8245300232ed99258 Mon Sep 17 00:00:00 2001 From: Gaith Hallak Date: Tue, 7 Sep 2021 17:45:28 +0300 Subject: [PATCH] Implement loading namespaces with the `using` keyword (#338) * Add using namespace as to scanner and parser * Change the alias from id() to con() * Add using namespace to AST type inference * Allow using namespace to appear in the top level * Allow using namespace to appear inside functions * Add a compiler test for using namespace * Handle name collisions * Implement mk_error for ambiguous_name * Add failing test for ambiguous names * Limit the scope of the used namespaces * Add test for wrong scope of using namespace * Use a single using declaration * Split long line * Forbid using undefined namespaces * Add a test for using undefined namespaces * Change the type of used_namespaces * Add using namespace parts to scanner and parser * Add using namespace parts to ast type inference * Add tests for using namespace parts * Update CHANGELOG.md * Code cleaning * Update the docs * Update the docs about the same alias for multiple namespaces --- CHANGELOG.md | 1 + docs/sophia_features.md | 60 +++++++++ src/aeso_ast_infer_types.erl | 127 +++++++++++++++--- src/aeso_parser.erl | 19 ++- src/aeso_scan.erl | 2 +- src/aeso_syntax.erl | 5 +- test/aeso_compiler_tests.erl | 25 ++++ test/contracts/using_namespace.aes | 36 +++++ .../using_namespace_ambiguous_name.aes | 13 ++ .../using_namespace_hidden_parts.aes | 8 ++ test/contracts/using_namespace_undefined.aes | 4 + .../using_namespace_undefined_parts.aes | 7 + .../contracts/using_namespace_wrong_scope.aes | 21 +++ 13 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 test/contracts/using_namespace.aes create mode 100644 test/contracts/using_namespace_ambiguous_name.aes create mode 100644 test/contracts/using_namespace_hidden_parts.aes create mode 100644 test/contracts/using_namespace_undefined.aes create mode 100644 test/contracts/using_namespace_undefined_parts.aes create mode 100644 test/contracts/using_namespace_wrong_scope.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f6ee0..9d97cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Set` stdlib - `Option.force_msg` +- Loading namespaces into the current scope (e.g. `using Pair`) ### Changed ### Removed diff --git a/docs/sophia_features.md b/docs/sophia_features.md index 6be7ac4..d6d062b 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -248,6 +248,66 @@ Functions in namespaces have access to the same environment (including the with the exception of `state`, `put` and `Chain.event` since these are dependent on the specific state and event types of the contract. +To avoid mentioning the namespace every time it is used, Sophia allows +including the namespace in the current scope with the `using` keyword: +``` +include "Pair.aes" +using Pair +contract C = + type state = int + entrypoint init() = + let p = (1, 2) + fst(p) // this is the same as Pair.fst(p) +``` + +It is also possible to make an alias for the namespace with the `as` keyword: +``` +include "Pair.aes" +contract C = + using Pair as P + type state = int + entrypoint init() = + let p = (1, 2) + P.fst(p) // this is the same as Pair.fst(p) +``` + +Having the same alias for multiple namespaces is possible and it allows +referening functions that are defined in different namespaces and have +different names with the same alias: +``` +namespace Xa = function f() = 1 +namespace Xb = function g() = 2 +contract Cntr = + using Xa as A + using Xb as A + type state = int + entrypoint init() = A.f() + A.g() +``` + +Note that using functions with the same name would result in an ambiguous name +error: +``` +namespace Xa = function f() = 1 +namespace Xb = function f() = 2 +contract Cntr = + using Xa as A + using Xb as A + type state = int + + // the next line has an error because f is defined in both Xa and Xb + entrypoint init() = A.f() +``` + +Importing specific parts of a namespace or hiding these parts can also be +done like this: +``` +using Pair for [fst, snd] // this will only import fst and snd +using Triple hiding [fst, snd] // this will import everything except for fst and snd +``` + +Note that it is possible to use a namespace in the top level of the file, in the +contract level, namespace level, or in the function level. + ## Splitting code over multiple files Code from another file can be included in a contract using an `include` diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index bc049cf..a0c07fa 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -102,6 +102,10 @@ -type qname() :: [string()]. -type typesig() :: {type_sig, aeso_syntax:ann(), type_constraints(), [aeso_syntax:named_arg_t()], [type()], type()}. +-type namespace_alias() :: none | name(). +-type namespace_parts() :: none | {for, [name()]} | {hiding, [name()]}. +-type used_namespaces() :: [{qname(), namespace_alias(), namespace_parts()}]. + -type type_constraints() :: none | bytes_concat | bytes_split | address_to_contract | bytecode_hash. -type fun_info() :: {aeso_syntax:ann(), typesig() | type()}. @@ -121,15 +125,16 @@ -type scope() :: #scope{}. -record(env, - { scopes = #{ [] => #scope{}} :: #{ qname() => scope() } - , vars = [] :: [{name(), var_info()}] - , typevars = unrestricted :: unrestricted | [name()] - , fields = #{} :: #{ name() => [field_info()] } %% fields are global - , namespace = [] :: qname() - , in_pattern = false :: boolean() - , stateful = false :: boolean() - , current_function = none :: none | aeso_syntax:id() - , what = top :: top | namespace | contract | contract_interface + { scopes = #{ [] => #scope{}} :: #{ qname() => scope() } + , vars = [] :: [{name(), var_info()}] + , typevars = unrestricted :: unrestricted | [name()] + , fields = #{} :: #{ name() => [field_info()] } %% fields are global + , namespace = [] :: qname() + , used_namespaces = [] :: used_namespaces() + , in_pattern = false :: boolean() + , stateful = false :: boolean() + , current_function = none :: none | aeso_syntax:id() + , what = top :: top | namespace | contract | contract_interface }). -type env() :: #env{}. @@ -312,9 +317,38 @@ bind_contract({Contract, Ann, Id, Contents}, Env) %% What scopes could a given name come from? -spec possible_scopes(env(), qname()) -> [qname()]. -possible_scopes(#env{ namespace = Current}, Name) -> +possible_scopes(#env{ namespace = Current, used_namespaces = UsedNamespaces }, Name) -> Qual = lists:droplast(Name), - [ lists:sublist(Current, I) ++ Qual || I <- lists:seq(0, length(Current)) ]. + NewQuals = case lists:filter(fun(X) -> element(2, X) == Qual end, UsedNamespaces) of + [] -> + [Qual]; + Namespaces -> + lists:map(fun(X) -> element(1, X) end, Namespaces) + end, + Ret1 = [ lists:sublist(Current, I) ++ Q || I <- lists:seq(0, length(Current)), Q <- NewQuals ], + Ret2 = [ Namespace ++ Q || {Namespace, none, _} <- UsedNamespaces, Q <- NewQuals ], + lists:usort(Ret1 ++ Ret2). + +-spec visible_in_used_namespaces(used_namespaces(), qname()) -> boolean(). +visible_in_used_namespaces(UsedNamespaces, QName) -> + Qual = lists:droplast(QName), + Name = lists:last(QName), + case lists:filter(fun({Ns, _, _}) -> Qual == Ns end, UsedNamespaces) of + [] -> + true; + Namespaces -> + IsVisible = fun(Namespace) -> + case Namespace of + {_, _, {for, Names}} -> + lists:member(Name, Names); + {_, _, {hiding, Names}} -> + not lists:member(Name, Names); + _ -> + true + end + end, + lists:any(IsVisible, Namespaces) + end. -spec lookup_type(env(), type_id()) -> false | {qname(), type_info()}. lookup_type(Env, Id) -> @@ -341,7 +375,7 @@ lookup_env(Env, Kind, Ann, Name) -> end. -spec lookup_env1(env(), type | term, aeso_syntax:ann(), qname()) -> false | {qname(), fun_info()}. -lookup_env1(#env{ namespace = Current, scopes = Scopes }, Kind, Ann, QName) -> +lookup_env1(#env{ namespace = Current, used_namespaces = UsedNamespaces, scopes = Scopes }, Kind, Ann, QName) -> Qual = lists:droplast(QName), Name = lists:last(QName), AllowPrivate = lists:prefix(Qual, Current), @@ -365,7 +399,11 @@ lookup_env1(#env{ namespace = Current, scopes = Scopes }, Kind, Ann, QName) -> {Ann1, _} = E -> %% Check that it's not private (or we can see private funs) case not is_private(Ann1) orelse AllowPrivate of - true -> {QName, E}; + true -> + case visible_in_used_namespaces(UsedNamespaces, QName) of + true -> {QName, E}; + false -> false + end; false -> false end end @@ -803,6 +841,8 @@ infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) -> {Env1, Code1} = infer_contract_top(push_scope(namespace, Name, Env), namespace, Code, Options), Namespace1 = {namespace, Ann, Name, Code1}, infer1(pop_scope(Env1), Rest, [Namespace1 | Acc], Options); +infer1(Env, [Using = {using, _, _, _, _} | Rest], Acc, Options) -> + infer1(check_usings(Env, Using), Rest, Acc, Options); infer1(Env, [{pragma, _, _} | Rest], Acc, Options) -> %% Pragmas are checked in check_modifiers infer1(Env, Rest, Acc, Options). @@ -859,10 +899,13 @@ infer_contract(Env0, What, Defs0, Options) -> ({letfun, _, _, _, _, _}) -> function; ({fun_clauses, _, _, _, _}) -> function; ({fun_decl, _, _, _}) -> prototype; + ({using, _, _, _, _}) -> using; (_) -> unexpected end, Get = fun(K, In) -> [ Def || Def <- In, Kind(Def) == K ] end, - {Env1, TypeDefs} = check_typedefs(Env, Get(type, Defs)), + OldUsedNamespaces = Env#env.used_namespaces, + Env01 = check_usings(Env, Get(using, Defs)), + {Env1, TypeDefs} = check_typedefs(Env01, Get(type, Defs)), create_type_errors(), check_unexpected(Get(unexpected, Defs)), Env2 = @@ -884,11 +927,13 @@ infer_contract(Env0, What, Defs0, Options) -> DepGraph = maps:map(fun(_, Def) -> aeso_syntax_utils:used_ids(Def) end, FunMap), SCCs = aeso_utils:scc(DepGraph), {Env4, Defs1} = check_sccs(Env3, FunMap, SCCs, []), + %% Remove namespaces used in the current namespace + Env5 = Env4#env{ used_namespaces = OldUsedNamespaces }, %% Check that `init` doesn't read or write the state check_state_dependencies(Env4, Defs1), destroy_and_report_type_errors(Env4), %% Add inferred types of definitions - {Env4, TypeDefs ++ Decls ++ Defs1}. + {Env5, TypeDefs ++ Decls ++ Defs1}. %% Restructure blocks into multi-clause fundefs (`fun_clauses`). -spec process_blocks([aeso_syntax:decl()]) -> [aeso_syntax:decl()]. @@ -988,6 +1033,43 @@ check_typedef(Env, {variant_t, Cons}) -> {variant_t, [ {constr_t, Ann, Con, [ check_type(Env, Arg) || Arg <- Args ]} || {constr_t, Ann, Con, Args} <- Cons ]}. +check_usings(Env, []) -> + Env; +check_usings(Env = #env{ used_namespaces = UsedNamespaces }, [{using, Ann, Con, Alias, Parts} | Rest]) -> + AliasName = case Alias of + none -> + none; + _ -> + qname(Alias) + end, + case get_scope(Env, qname(Con)) of + false -> + create_type_errors(), + type_error({using_undefined_namespace, Ann, qname(Con)}), + destroy_and_report_type_errors(Env); + Scope -> + Nsp = case Parts of + none -> + {qname(Con), AliasName, none}; + {ForOrHiding, Ids} -> + IsUndefined = fun(Id) -> + proplists:lookup(name(Id), Scope#scope.funs) == none + end, + UndefinedIds = lists:filter(IsUndefined, Ids), + case UndefinedIds of + [] -> + {qname(Con), AliasName, {ForOrHiding, lists:map(fun name/1, Ids)}}; + _ -> + create_type_errors(), + type_error({using_undefined_namespace_parts, Ann, qname(Con), lists:map(fun qname/1, UndefinedIds)}), + destroy_and_report_type_errors(Env) + end + end, + check_usings(Env#env{ used_namespaces = UsedNamespaces ++ [Nsp] }, Rest) + end; +check_usings(Env, Using = {using, _, _, _, _}) -> + check_usings(Env, [Using]). + check_unexpected(Xs) -> [ type_error(X) || X <- Xs ]. @@ -1017,6 +1099,8 @@ check_modifiers_(Env, [{namespace, _, _, Decls} | Rest]) -> check_modifiers_(Env, [{pragma, Ann, Pragma} | Rest]) -> check_pragma(Env, Ann, Pragma), check_modifiers_(Env, Rest); +check_modifiers_(Env, [{using, _, _, _, _} | Rest]) -> + check_modifiers_(Env, Rest); check_modifiers_(Env, [Decl | Rest]) -> type_error({bad_top_level_decl, Decl}), check_modifiers_(Env, Rest); @@ -1770,6 +1854,8 @@ infer_block(Env, _, [{letval, Attrs, Pattern, E}|Rest], BlockType) -> {'case', _, NewPattern, {typed, _, {block, _, NewRest}, _}} = infer_case(Env, Attrs, Pattern, PatType, {block, Attrs, Rest}, BlockType), [{letval, Attrs, NewPattern, NewE}|NewRest]; +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)]. @@ -2987,6 +3073,17 @@ mk_error({contract_lacks_definition, Type, When}) -> ), {Pos, Ctxt} = pp_when(When), mk_t_err(Pos, Msg, Ctxt); +mk_error({ambiguous_name, QIds = [{qid, Ann, _} | _]}) -> + Names = lists:map(fun(QId) -> io_lib:format("~s at ~s\n", [pp(QId), pp_loc(QId)]) end, QIds), + Msg = "Ambiguous name: " ++ lists:concat(Names), + mk_t_err(pos(Ann), Msg); +mk_error({using_undefined_namespace, Ann, Namespace}) -> + Msg = io_lib:format("Cannot use undefined namespace ~s", [Namespace]), + mk_t_err(pos(Ann), Msg); +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(Err) -> Msg = io_lib:format("Unknown error: ~p\n", [Err]), mk_t_err(pos(0, 0), Msg). diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index c29403d..80002ca 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -109,6 +109,7 @@ decl() -> , ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4}) , ?RULE(keyword(include), str(), {include, get_ann(_1), _2}) + , using() , pragma() %% Type declarations TODO: format annotation for "type bla" vs "type bla()" @@ -135,6 +136,21 @@ fundef_or_decl() -> choice([?RULE(id(), tok(':'), type(), {fun_decl, get_ann(_1), _1, _3}), fundef()]). +using() -> + Alias = {keyword(as), con()}, + For = ?RULE(keyword(for), bracket_list(id()), {for, _2}), + Hiding = ?RULE(keyword(hiding), bracket_list(id()), {hiding, _2}), + ?RULE(keyword(using), con(), optional(Alias), optional(choice(For, Hiding)), using(get_ann(_1), _2, _3, _4)). + +using(Ann, Con, none, none) -> + {using, Ann, Con, none, none}; +using(Ann, Con, {ok, {_, Alias}}, none) -> + {using, Ann, Con, Alias, none}; +using(Ann, Con, none, {ok, List}) -> + {using, Ann, Con, none, List}; +using(Ann, Con, {ok, {_, Alias}}, {ok, List}) -> + {using, Ann, Con, Alias, List}. + pragma() -> Op = choice([token(T) || T <- ['<', '=<', '==', '>=', '>']]), ?RULE(tok('@'), id("compiler"), Op, version(), {pragma, get_ann(_1), {compiler, element(1, _3), _4}}). @@ -254,7 +270,8 @@ body() -> stmt() -> ?LAZY_P(choice( - [ expr() + [ using() + , expr() , letdecl() , {switch, keyword(switch), parens(expr()), maybe_block(branch())} , {'if', keyword('if'), parens(expr()), body()} diff --git a/src/aeso_scan.erl b/src/aeso_scan.erl index 2c5d301..4587efa 100644 --- a/src/aeso_scan.erl +++ b/src/aeso_scan.erl @@ -45,7 +45,7 @@ lexer() -> Keywords = ["contract", "include", "let", "switch", "type", "record", "datatype", "if", "elif", "else", "function", "stateful", "payable", "true", "false", "mod", "public", "entrypoint", "private", "indexed", "namespace", - "interface", "main" + "interface", "main", "using", "as", "for", "hiding" ], KW = string:join(Keywords, "|"), diff --git a/src/aeso_syntax.erl b/src/aeso_syntax.erl index cab8edc..8148a1a 100644 --- a/src/aeso_syntax.erl +++ b/src/aeso_syntax.erl @@ -35,6 +35,9 @@ -type qcon() :: {qcon, ann(), [name()]}. -type tvar() :: {tvar, ann(), name()}. +-type namespace_alias() :: none | con(). +-type namespace_parts() :: none | {for, [id()]} | {hiding, [id()]}. + -type decl() :: {contract_main, ann(), con(), [decl()]} | {contract_child, ann(), con(), [decl()]} | {contract_interface, ann(), con(), [decl()]} @@ -44,6 +47,7 @@ | {type_def, ann(), id(), [tvar()], typedef()} | {fun_clauses, ann(), id(), type(), [letfun() | fundecl()]} | {block, ann(), [decl()]} + | {using, ann(), con(), namespace_alias(), namespace_parts()} | fundecl() | letfun() | letval(). % Only for error msgs @@ -52,7 +56,6 @@ -type pragma() :: {compiler, '==' | '<' | '>' | '=<' | '>=', compiler_version()}. - -type letval() :: {letval, ann(), pat(), expr()}. -type letfun() :: {letfun, ann(), id(), [pat()], type(), expr()}. -type fundecl() :: {fun_decl, ann(), id(), type()}. diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 1df9bf3..72b10cc 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -200,6 +200,7 @@ compilable_contracts() -> "clone_simple", "create", "child_contract_init_bug", + "using_namespace", "test" % Custom general-purpose test file. Keep it last on the list. ]. @@ -781,6 +782,30 @@ failing_contracts() -> [<> ]) + , ?TYPE_ERROR(using_namespace_ambiguous_name, + [ <> + , <> + ]) + , ?TYPE_ERROR(using_namespace_wrong_scope, + [ <> + , <> + ]) + , ?TYPE_ERROR(using_namespace_undefined, + [<> + ]) + , ?TYPE_ERROR(using_namespace_undefined_parts, + [<> + ]) + , ?TYPE_ERROR(using_namespace_hidden_parts, + [<> + ]) ]. -define(Path(File), "code_errors/" ??File). diff --git a/test/contracts/using_namespace.aes b/test/contracts/using_namespace.aes new file mode 100644 index 0000000..95d40e5 --- /dev/null +++ b/test/contracts/using_namespace.aes @@ -0,0 +1,36 @@ +include "Option.aes" +include "Pair.aes" +include "String.aes" +include "Triple.aes" + +using Pair +using Triple hiding [fst, snd] + +namespace Nsp = + using Option + + function h() = + let op = Some((2, 3, 4)) + if (is_some(op)) + thd(force(op)) == 4 + else + false + +contract Cntr = + using Nsp + + entrypoint init() = () + + function f() = + let p = (1, 2) + if (h()) + fst(p) + else + snd(p) + + function g() = + using String for [concat] + + let s1 = "abc" + let s2 = "def" + concat(s1, s2) diff --git a/test/contracts/using_namespace_ambiguous_name.aes b/test/contracts/using_namespace_ambiguous_name.aes new file mode 100644 index 0000000..e86e8ad --- /dev/null +++ b/test/contracts/using_namespace_ambiguous_name.aes @@ -0,0 +1,13 @@ +namespace Xa = + function f() = 1 + +namespace Xb = + function f() = 2 + +contract Cntr = + using Xa as A + using Xb as A + + type state = int + + entrypoint init() = A.f() diff --git a/test/contracts/using_namespace_hidden_parts.aes b/test/contracts/using_namespace_hidden_parts.aes new file mode 100644 index 0000000..5469e95 --- /dev/null +++ b/test/contracts/using_namespace_hidden_parts.aes @@ -0,0 +1,8 @@ +namespace Nsp = + function f() = 1 + function g() = 2 + +contract Cntr = + using Nsp for [f] + + entrypoint init() = g() diff --git a/test/contracts/using_namespace_undefined.aes b/test/contracts/using_namespace_undefined.aes new file mode 100644 index 0000000..130fd92 --- /dev/null +++ b/test/contracts/using_namespace_undefined.aes @@ -0,0 +1,4 @@ +contract C = + using MyUndefinedNamespace + + entrypoint init() = () diff --git a/test/contracts/using_namespace_undefined_parts.aes b/test/contracts/using_namespace_undefined_parts.aes new file mode 100644 index 0000000..a728641 --- /dev/null +++ b/test/contracts/using_namespace_undefined_parts.aes @@ -0,0 +1,7 @@ +namespace Nsp = + function f() = 1 + +contract Cntr = + using Nsp for [a] + + entrypoint init() = f() diff --git a/test/contracts/using_namespace_wrong_scope.aes b/test/contracts/using_namespace_wrong_scope.aes new file mode 100644 index 0000000..8e8c298 --- /dev/null +++ b/test/contracts/using_namespace_wrong_scope.aes @@ -0,0 +1,21 @@ +namespace Nsp1 = + function f() = 1 + +namespace Nsp2 = + using Nsp1 + + function g() = 1 + +contract Cntr = + using Nsp2 + + type state = int + + function x() = + using Nsp1 + f() + + function y() = + f() + + entrypoint init() = f()