From 2b7490776eeb5e1deebbc3c2ac73afff9cc369dc Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Tue, 5 Feb 2019 08:52:40 +0100 Subject: [PATCH] Add include directive Add an include directive to include namespaces into a contract. Only allowed at the top level. To allow includes, either call through aeso_compiler:file or set the option `allow_include` (and add `include_path`(s)). --- .gitignore | 1 + src/aeso_compiler.erl | 43 ++++++++++++----- src/aeso_parse_lib.erl | 2 +- src/aeso_parser.erl | 93 ++++++++++++++++++++++++++++-------- src/aeso_scan.erl | 2 +- test/aeso_compiler_tests.erl | 21 ++++++-- test/contracts/include.aes | 10 ++++ test/contracts/included.aes | 2 + test/contracts/included2.aes | 5 ++ 9 files changed, 139 insertions(+), 40 deletions(-) create mode 100644 test/contracts/include.aes create mode 100644 test/contracts/included.aes create mode 100644 test/contracts/included2.aes diff --git a/.gitignore b/.gitignore index 695011d..3b6ef01 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ _build rebar3.crashdump *.erl~ *.aes~ +aesophia diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 47937ab..6967a88 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -21,8 +21,16 @@ -include("aeso_icode.hrl"). --type option() :: pp_sophia_code | pp_ast | pp_types | pp_typed_ast | - pp_icode| pp_assembler | pp_bytecode. +-type option() :: pp_sophia_code + | pp_ast + | pp_types + | pp_typed_ast + | pp_icode + | pp_assembler + | pp_bytecode + | {include_path, [string()]} + | {allow_include, boolean()} + | {src_file, string()}. -type options() :: [option()]. -export_type([ option/0 @@ -40,12 +48,13 @@ version() -> -spec file(string()) -> {ok, map()} | {error, binary()}. file(Filename) -> - file(Filename, []). + Dir = filename:dirname(Filename), + file(Filename, [{include_path, [Dir]}]). -spec file(string(), options()) -> {ok, map()} | {error, binary()}. file(File, Options) -> case read_contract(File) of - {ok, Bin} -> from_string(Bin, Options); + {ok, Bin} -> from_string(Bin, [{src_file, File}, {allow_include, true} | Options]); {error, Error} -> ErrorString = [File,": ",file:format_error(Error)], {error, join_errors("File errors", [ErrorString], fun(E) -> E end)} @@ -213,9 +222,6 @@ icode_to_term(T, V) -> icodes_to_terms(Ts, Vs) -> [ icode_to_term(T, V) || {T, V} <- lists:zip(Ts, Vs) ]. -parse(C,_Options) -> - parse_string(C). - to_icode(TypedAst, Options) -> aeso_ast_to_icode:convert_typed(TypedAst, Options). @@ -265,9 +271,9 @@ sophia_type_to_typerep(String) -> catch _:_ -> {error, bad_type} end. -parse_string(Text) -> +parse(Text, Options) -> %% Try and return something sensible here! - case aeso_parser:string(Text) of + case aeso_parser:string(Text, Options) of %% Yay, it worked! {ok, Contract} -> Contract; %% Scan errors. @@ -280,12 +286,25 @@ parse_string(Text) -> parse_error(Pos, Error); {error, {Pos, ambiguous_parse, As}} -> ErrorString = io_lib:format("Ambiguous ~p", [As]), - parse_error(Pos, ErrorString) + parse_error(Pos, ErrorString); + %% Include error + {error, {Pos, include_not_allowed}} -> + parse_error(Pos, "includes not allowed in this context"); + {error, {Pos, include_error}} -> + parse_error(Pos, "could not find include file") end. -parse_error({Line, Pos}, ErrorString) -> - Error = io_lib:format("line ~p, column ~p: ~s", [Line, Pos, ErrorString]), +parse_error(Pos, ErrorString) -> + Error = io_lib:format("~s: ~s", [pos_error(Pos), ErrorString]), error({parse_errors, [Error]}). read_contract(Name) -> file:read_file(Name). + +pos_error({Line, Pos}) -> + io_lib:format("line ~p, column ~p", [Line, Pos]); +pos_error({no_file, Line, Pos}) -> + pos_error({Line, Pos}); +pos_error({File, Line, Pos}) -> + io_lib:format("file ~s, line ~p, column ~p", [File, Line, Pos]). + diff --git a/src/aeso_parse_lib.erl b/src/aeso_parse_lib.erl index d1be781..21f3df5 100644 --- a/src/aeso_parse_lib.erl +++ b/src/aeso_parse_lib.erl @@ -19,7 +19,7 @@ -export_type([parser/1, parser_expr/1, pos/0, token/0, tokens/0]). --type pos() :: {integer(), integer()}. +-type pos() :: {string() | no_file, integer(), integer()} | {integer(), integer()}. -type token() :: {atom(), pos(), term()} | {atom(), pos()}. -type tokens() :: [token()]. -type error() :: {pos(), string() | no_error}. diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 7b7e7bd..fc13e54 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -5,28 +5,37 @@ -module(aeso_parser). -export([string/1, + string/2, type/1]). -include("aeso_parse_lib.hrl"). --spec string(string()) -> - {ok, aeso_syntax:ast()} - | {error, {aeso_parse_lib:pos(), - atom(), - term()}} - | {error, {aeso_parse_lib:pos(), - atom()}}. +-type parse_result() :: {ok, aeso_syntax:ast()} + | {error, {aeso_parse_lib:pos(), atom(), term()}} + | {error, {aeso_parse_lib:pos(), atom()}}. + +-spec string(string()) -> parse_result(). string(String) -> - parse_and_scan(file(), String). + string(String, []). + +-spec string(string(), aeso_compiler:options()) -> parse_result(). +string(String, Opts) -> + case parse_and_scan(file(), String, Opts) of + {ok, AST} -> + expand_includes(AST, Opts); + Err = {error, _} -> + Err + end. type(String) -> - parse_and_scan(type(), String). + parse_and_scan(type(), String, []). -parse_and_scan(P, S) -> - case aeso_scan:scan(S) of - {ok, Tokens} -> aeso_parse_lib:parse(P, Tokens); - Error -> Error - end. +parse_and_scan(P, S, Opts) -> + set_current_file(proplists:get_value(src_file, Opts, no_file)), + case aeso_scan:scan(S) of + {ok, Tokens} -> aeso_parse_lib:parse(P, Tokens); + Error -> Error + end. %% -- Parsing rules ---------------------------------------------------------- @@ -38,6 +47,7 @@ decl() -> %% Contract declaration [ ?RULE(keyword(contract), con(), tok('='), maybe_block(decl()), {contract, _1, _2, _4}) , ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4}) + , ?RULE(keyword(include), str(), {include, _2}) %% Type declarations TODO: format annotation for "type bla" vs "type bla()" , ?RULE(keyword(type), id(), {type_decl, _1, _2, []}) @@ -302,6 +312,7 @@ binop(Ops) -> con() -> token(con). id() -> token(id). tvar() -> token(tvar). +str() -> token(string). token(Tag) -> ?RULE(tok(Tag), @@ -337,10 +348,17 @@ 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) -> [{line, Line}, {col, Col}]. +pos_ann(Line, Col) -> [{file, current_file()}, {line, Line}, {col, Col}]. + +current_file() -> + get('$current_file'). + +set_current_file(File) -> + put('$current_file', File). ann_pos(Ann) -> - {proplists:get_value(line, Ann), + {proplists:get_value(file, Ann), + proplists:get_value(line, Ann), proplists:get_value(col, Ann)}. get_ann(Ann) when is_list(Ann) -> Ann; @@ -358,10 +376,10 @@ set_ann(Key, Val, Node) -> setelement(2, Node, lists:keystore(Key, 1, Ann, {Key, Val})). get_pos(Node) -> - {get_ann(line, Node), get_ann(col, Node)}. + {current_file(), get_ann(line, Node), get_ann(col, Node)}. -set_pos({L, C}, Node) -> - set_ann(line, L, set_ann(col, C, Node)). +set_pos({F, L, C}, Node) -> + set_ann(file, F, set_ann(line, L, set_ann(col, C, Node))). infix(L, Op, R) -> set_ann(format, infix, {app, get_ann(L), Op, [L, R]}). @@ -443,8 +461,10 @@ parse_pattern(E) -> bad_expr_err("Not a valid pattern", E). parse_field_pattern({field, Ann, F, E}) -> {field, Ann, F, parse_pattern(E)}. -return_error({L, C}, Err) -> - fail(io_lib:format("~p:~p:\n~s", [L, C, Err])). +return_error({no_file, L, C}, Err) -> + fail(io_lib:format("~p:~p:\n~s", [L, C, Err])); +return_error({F, L, C}, Err) -> + fail(io_lib:format("In ~s at ~p:~p:\n~s", [F, L, C, Err])). -spec ret_doc_err(ann(), prettypr:document()) -> no_return(). ret_doc_err(Ann, Doc) -> @@ -456,3 +476,34 @@ bad_expr_err(Reason, E) -> prettypr:sep([prettypr:text(Reason ++ ":"), prettypr:nest(2, aeso_pretty:expr(E))])). +%% -- Helper functions ------------------------------------------------------- +expand_includes(AST, Opts) -> + expand_includes(AST, [], Opts). + +expand_includes([], Acc, _Opts) -> + {ok, lists:reverse(Acc)}; +expand_includes([{include, S = {string, _, File}} | AST], Acc, Opts) -> + AllowInc = proplists:get_value(allow_include, Opts, false), + case read_file(File, Opts) of + {ok, Bin} when AllowInc -> + Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}), + case string(binary_to_list(Bin), Opts1) of + {ok, AST1} -> + expand_includes(AST1 ++ AST, Acc, Opts); + Err = {error, _} -> + Err + end; + {ok, _} -> + {error, {get_pos(S), include_not_allowed}}; + {error, _} -> + {error, {get_pos(S), include_error}} + end; +expand_includes([E | AST], Acc, Opts) -> + expand_includes(AST, [E | Acc], Opts). + +read_file(File, Opts) -> + CandidateNames = [File] ++ [ filename:join(Dir, File) + || Dir <- proplists:get_value(include_path, Opts, []) ], + lists:foldr(fun(F, {error, _}) -> file:read_file(F); + (_F, OK) -> OK end, {error, not_found}, CandidateNames). + diff --git a/src/aeso_scan.erl b/src/aeso_scan.erl index 617285d..17b26bc 100644 --- a/src/aeso_scan.erl +++ b/src/aeso_scan.erl @@ -36,7 +36,7 @@ lexer() -> , {"\\*/", pop(skip())} , {"[^/*]+|[/*]", skip()} ], - Keywords = ["contract", "import", "let", "rec", "switch", "type", "record", "datatype", "if", "elif", "else", "function", + Keywords = ["contract", "include", "let", "rec", "switch", "type", "record", "datatype", "if", "elif", "else", "function", "stateful", "true", "false", "and", "mod", "public", "private", "indexed", "internal", "namespace"], KW = string:join(Keywords, "|"), diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 0c92123..1343c73 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -23,8 +23,12 @@ simple_compile_test_() -> end} || ContractName <- compilable_contracts() ] ++ [ {"Testing error messages of " ++ ContractName, fun() -> - <<"Type errors\n",ErrorString/binary>> = compile(ContractName), - check_errors(lists:sort(ExpectedErrors), ErrorString) + case compile(ContractName, false) of + <<"Type errors\n", ErrorString/binary>> -> + check_errors(lists:sort(ExpectedErrors), ErrorString); + <<"Parse errors\n", ErrorString/binary>> -> + check_errors(lists:sort(ExpectedErrors), ErrorString) + end end} || {ContractName, ExpectedErrors} <- failing_contracts() ] ++ [ {"Testing deadcode elimination", @@ -46,9 +50,13 @@ check_errors(Expect, ErrorString) -> {Missing, Extra} -> ?assertEqual(Missing, Extra) end. -compile(Name) -> +compile(Name) -> compile(Name, true). + +compile(Name, AllowInc) -> String = aeso_test_utils:read_contract(Name), - case aeso_compiler:from_string(String, []) of + case aeso_compiler:from_string(String, [{include_path, [aeso_test_utils:contract_path()]}, + {allow_include, AllowInc}, + {src_file, Name}]) of {ok,Map} -> Map; {error,ErrorString} -> ErrorString end. @@ -78,7 +86,8 @@ compilable_contracts() -> "deadcode", "variant_types", "state_handling", - "events" + "events", + "include" ]. %% Contracts that should produce type errors @@ -192,4 +201,6 @@ failing_contracts() -> " r.foo : (gas : int, value : int) => Remote.themap\n" "against the expected type\n" " (gas : int, value : int) => map(string, int)">>]} + , {"include", + [<<"file include, line 1, column 9: includes not allowed in this context\n">>]} ]. diff --git a/test/contracts/include.aes b/test/contracts/include.aes new file mode 100644 index 0000000..2431aa5 --- /dev/null +++ b/test/contracts/include.aes @@ -0,0 +1,10 @@ +include "included.aes" +include "../contracts/included2.aes" + +contract Include = + // include "maps.aes" + function foo() = + Included.foo() < Included2a.bar() + + function bar() = + Included2b.foo() > Included.foo() diff --git a/test/contracts/included.aes b/test/contracts/included.aes new file mode 100644 index 0000000..5e229b2 --- /dev/null +++ b/test/contracts/included.aes @@ -0,0 +1,2 @@ +namespace Included = + function foo() = 42 diff --git a/test/contracts/included2.aes b/test/contracts/included2.aes new file mode 100644 index 0000000..85d9b07 --- /dev/null +++ b/test/contracts/included2.aes @@ -0,0 +1,5 @@ +namespace Included2a = + function bar() = 43 + +namespace Included2b = + function foo() = 44