From fe1a2758c3d6d06b9c8cf2dbce8d55561cc411cb Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Fri, 11 Jan 2019 17:36:31 +0100 Subject: [PATCH 1/4] Improve the interface to the compiler It is now more consistent though we can still discuss how we want the interface to look. --- src/aeso_compiler.erl | 134 +++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index e132005..2e2b27a 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -21,8 +21,8 @@ -include("aeso_icode.hrl"). --type option() :: pp_sophia_code | pp_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. -type options() :: [option()]. -export_type([ option/0 @@ -43,29 +43,51 @@ file(Filename) -> file(Filename, []). -spec file(string(), options()) -> map(). -file(Filename, Options) -> - C = read_contract(Filename), - from_string(C, Options). +file(File, Options) -> + case read_contract(File, Options) of + {ok,Bin} -> from_string(Bin, Options); + {error,Error} -> {error,{File,Error}} + end. --spec from_string(string(), options()) -> map(). +-spec from_string(binary() | string(), options()) -> map(). +from_string(ContractBin, Options) when is_binary(ContractBin) -> + from_string(binary_to_list(ContractBin), Options); from_string(ContractString, Options) -> - Ast = parse(ContractString, Options), - ok = pp_sophia_code(Ast, Options), - ok = pp_ast(Ast, Options), - TypedAst = aeso_ast_infer_types:infer(Ast, Options), - %% pp_types is handled inside aeso_ast_infer_types. - ok = pp_typed_ast(TypedAst, Options), - ICode = to_icode(TypedAst, Options), - TypeInfo = extract_type_info(ICode), - ok = pp_icode(ICode, Options), - Assembler = assemble(ICode, Options), - ok = pp_assembler(Assembler, Options), - ByteCodeList = to_bytecode(Assembler, Options), - ByteCode = << << B:8 >> || B <- ByteCodeList >>, - ok = pp_bytecode(ByteCode, Options), - #{byte_code => ByteCode, type_info => TypeInfo, - contract_source => ContractString, - compiler_version => version()}. + try + Ast = parse(ContractString, Options), + ok = pp_sophia_code(Ast, Options), + ok = pp_ast(Ast, Options), + TypedAst = aeso_ast_infer_types:infer(Ast, Options), + %% pp_types is handled inside aeso_ast_infer_types. + ok = pp_typed_ast(TypedAst, Options), + ICode = to_icode(TypedAst, Options), + TypeInfo = extract_type_info(ICode), + ok = pp_icode(ICode, Options), + Assembler = assemble(ICode, Options), + ok = pp_assembler(Assembler, Options), + ByteCodeList = to_bytecode(Assembler, Options), + ByteCode = << << B:8 >> || B <- ByteCodeList >>, + ok = pp_bytecode(ByteCode, Options), + {ok,#{byte_code => ByteCode, + compiler_version => version(), + contract_source => ContractString, + type_info => TypeInfo + }} + catch + %% The compiler errors. + error:{parse_errors,Errors} -> + {error,join_errors("Parse errors", Errors, fun(E) -> E end)}; + error:{type_errors,Errors} -> + {error,join_errors("Type errors", Errors, fun(E) -> E end)}; + error:{code_errors,Errors} -> + {error,join_errors("Code errors", Errors, + fun (E) -> io_lib:format("~p", [E]) end)} + %% General programming errors in the compiler just signal error. + end. + +join_errors(Prefix, Errors, Pfun) -> + Ess = [ Pfun(E) || E <- Errors ], + list_to_binary(string:join([Prefix|Ess], "\n")). -define(CALL_NAME, "__call"). @@ -76,26 +98,36 @@ from_string(ContractString, Options) -> -spec check_call(string(), options()) -> {ok, string(), {[Type], Type | any}, [term()]} | {error, term()} when Type :: term(). check_call(ContractString, Options) -> - Ast = parse(ContractString, Options), - ok = pp_sophia_code(Ast, Options), - ok = pp_ast(Ast, Options), - TypedAst = aeso_ast_infer_types:infer(Ast, [permissive_address_literals]), - {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), - ok = pp_typed_ast(TypedAst, Options), - Icode = to_icode(TypedAst, Options), - ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ], - RetVMType = case RetType of - {id, _, "_"} -> any; - _ -> aeso_ast_to_icode:ast_typerep(RetType, Icode) - end, - ok = pp_icode(Icode, Options), - #{ functions := Funs } = Icode, - ArgIcode = get_arg_icode(Funs), - try [ icode_to_term(T, Arg) || {T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ] of - ArgTerms -> - {ok, FunName, {ArgVMTypes, RetVMType}, ArgTerms} - catch throw:Err -> - {error, Err} + try + Ast = parse(ContractString, Options), + ok = pp_sophia_code(Ast, Options), + ok = pp_ast(Ast, Options), + TypedAst = aeso_ast_infer_types:infer(Ast, [permissive_address_literals]), + {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), + ok = pp_typed_ast(TypedAst, Options), + Icode = to_icode(TypedAst, Options), + ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ], + RetVMType = case RetType of + {id, _, "_"} -> any; + _ -> aeso_ast_to_icode:ast_typerep(RetType, Icode) + end, + ok = pp_icode(Icode, Options), + #{ functions := Funs } = Icode, + ArgIcode = get_arg_icode(Funs), + ArgTerms = [ icode_to_term(T, Arg) || + {T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ], + {ok, FunName, {ArgVMTypes, RetVMType}, ArgTerms} + catch + error:{parse_errors, Errors} -> + {error,join_errors("Parse errors", Errors, fun (E) -> E end)}; + error:{type_errors, Errors} -> + {error,join_errors("Type errors", Errors, fun (E) -> E end)}; + error:{badmatch,{error,missing_call_function}} -> + {error,join_errors("Type errors", ["missing __call function"], + fun (E) -> E end)}; + throw:Error -> %Don't ask + {error,join_errors("Code errors", [Error], + fun (E) -> io_lib:format("~p", [E]) end)} end. -spec create_calldata(map(), string(), string()) -> @@ -113,7 +145,7 @@ create_calldata(Contract, Function, Argument) when is_map(Contract) -> %% Function should be "foo : type", and %% Argument should be "Arg1, Arg2, .., ArgN" (no parens) case string:lexemes(Function, ": ") of - %% If function is a single word fallback to old calldata generation + %% If function is a single word fallback to old calldata generation [FunName] -> aeso_abi:old_create_calldata(Contract, FunName, Argument); [FunName | _] -> Args = lists:map(fun($\n) -> 32; (X) -> X end, Argument), %% newline to space @@ -251,9 +283,13 @@ parse_error({Line,Pos}, ErrorString) -> Error = io_lib:format("line ~p, column ~p: ~s", [Line,Pos,ErrorString]), error({parse_errors,[Error]}). -read_contract(Name) -> - {ok, Bin} = file:read_file(filename:join(contract_path(), lists:concat([Name, ".aes"]))), - binary_to_list(Bin). +read_contract(Name, Opts) -> + FileName = filename(Name, ".aes", Opts), + file:read_file(FileName). -contract_path() -> - "apps/aesophia/test/contracts". +filename(File, Suffix, _Opts) -> + Base = filename:basename(File, Suffix), + Sdir = filename:dirname(File), + if Sdir == "." -> Base ++ Suffix; + true -> filename:join(Sdir, Base ++ Suffix) + end. -- 2.30.2 From 70ad303e160f454a838ddbe663bdc9574e85b7f5 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Fri, 11 Jan 2019 17:40:20 +0100 Subject: [PATCH 2/4] Document the aeso_compiler module The module doc format is loosely based on the standard erlang doc formats. --- .gitignore | 2 ++ README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/.gitignore b/.gitignore index 40ca652..695011d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ _build .idea *.iml rebar3.crashdump +*.erl~ +*.aes~ diff --git a/README.md b/README.md index d29c484..b8d9720 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,88 @@ It is an OTP application written in Erlang and is by default included in [the æternity node](https://github.com/aeternity/epoch). However, it can also be included in other system to compile contracts coded in sophia which can then be loaded into the æternity system. + +## Modules + +### aeso_compiler + +The Sophia compiler + +### Description + +This module provides the interface to the standard Sophia compiler. It +returns the compiled module in a map which can then be loaded. + +### Types +```erlang +contract_string() = string() | binary() +contract_map() = #{bytecode => binary(), + compiler_version => string(), + contract_souce => string(), + type_info => type_info()} +type_info() +errorstring() = binary() +``` +### Exports + +#### file(File) +#### file(File, Options) -> CompRet +#### from_string(ContractString, Options) -> CompRet + +Types + +``` erlang +ContractString = contract_string() +Options = [Option] +CompRet = {ok,ContractMap} | {error,ErrorString} +ContractMap = contract_map() +ErrorString = errorstring() +``` + +Compile a contract defined in a file or in a string. + +The **pp_** options all print to standard output the following: + +`pp_sophia_code` - print the input Sophia code. + +`pp_ast` - print the AST of the code + +`pp_types` - print information about the types + +`pp_typed_ast` - print the AST with type information at each node + +`pp_icode` - print the internal code structure + +`pp_assembler` - print the generated assembler code + +`pp_bytecode` - print the bytecode instructions + +#### check_call(ContractString, Options) -> CheckRet + +Types +``` +ContractString = string() | binary() +CheckRet = {ok,string(),{Types,Type | any()},Terms} | {error,Term} +Types = [Type] +Type = term() +``` +Check a call in contract through the `__call` function. + +#### sophia_type_to_typerep(String) -> TypeRep + +Types +``` erlang + {ok,TypeRep} | {error, badtype} +``` + +Get the type representation of a type declaration. + +#### version() -> Version + +Types + +``` erlang +Version = integer() +``` + +Get the current version of the Sophia compiler. -- 2.30.2 From db1c0fa05a49139ee80c96d33ed75bba63cdf95d Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Fri, 11 Jan 2019 17:41:54 +0100 Subject: [PATCH 3/4] Improve pretty printing the code AST --- src/aeso_ast.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/aeso_ast.erl b/src/aeso_ast.erl index ee25e15..90e6595 100644 --- a/src/aeso_ast.erl +++ b/src/aeso_ast.erl @@ -17,11 +17,12 @@ line({symbol, Line, _}) -> Line. symbol_name({symbol, _, Name}) -> Name. pp(Ast) -> - %% TODO: Actually do *Pretty* printing. - io:format("~p~n", [Ast]). + %% io:format("Tree:\n~p\n",[Ast]), + String = prettypr:format(aeso_pretty:decls(Ast, [])), + io:format("Ast:\n~s\n", [String]). pp_typed(TypedAst) -> + %% io:format("Typed tree:\n~p\n",[TypedAst]), String = prettypr:format(aeso_pretty:decls(TypedAst, [show_generated])), - %%io:format("Typed tree:\n~p\n",[TypedAst]), - io:format("Type info:\n~s\n",[String]). + io:format("Type ast:\n~s\n",[String]). -- 2.30.2 From a73abf8e8ebe16336abec7eca4cefb10d1c7fc46 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Fri, 25 Jan 2019 15:57:02 +0100 Subject: [PATCH 4/4] Fix testing to use new error message format --- test/aeso_compiler_tests.erl | 176 +++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 81 deletions(-) diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 3c462ab..fced305 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -28,13 +28,15 @@ simple_compile_test_() -> end} || ContractName <- compilable_contracts() ] ++ [ {"Testing error messages of " ++ ContractName, fun() -> - {type_errors, Errors} = compile(ContractName), - check_errors(lists:sort(ExpectedErrors), lists:sort(Errors)) + <<"Type errors\n",ErrorString/binary>> = compile(ContractName), + check_errors(lists:sort(ExpectedErrors), ErrorString) end} || {ContractName, ExpectedErrors} <- failing_contracts() ] }. -check_errors(Expect, Actual) -> +check_errors(Expect, ErrorString) -> + %% This removes the final single \n as well. + Actual = binary:split(<>, <<"\n\n">>, [global,trim]), case {Expect -- Actual, Actual -- Expect} of {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); @@ -42,10 +44,10 @@ check_errors(Expect, Actual) -> end. compile(Name) -> - try - aeso_compiler:from_string(aeso_test_utils:read_contract(Name), []) - catch _:{type_errors, _} = E -> - E + String = aeso_test_utils:read_contract(Name), + case aeso_compiler:from_string(String, []) of + {ok,Map} -> Map; + {error,ErrorString} -> ErrorString end. %% compilable_contracts() -> [ContractName]. @@ -75,82 +77,94 @@ compilable_contracts() -> failing_contracts() -> [ {"name_clash", - ["Duplicate definitions of abort at\n - (builtin location)\n - line 14, column 3\n", - "Duplicate definitions of double_def at\n - line 10, column 3\n - line 11, column 3\n", - "Duplicate definitions of double_proto at\n - line 4, column 3\n - line 5, column 3\n", - "Duplicate definitions of proto_and_def at\n - line 7, column 3\n - line 8, column 3\n", - "Duplicate definitions of put at\n - (builtin location)\n - line 15, column 3\n", - "Duplicate definitions of state at\n - (builtin location)\n - line 16, column 3\n"]} + [<<"Duplicate definitions of abort at\n" + " - (builtin location)\n" + " - line 14, column 3">>, + <<"Duplicate definitions of double_def at\n" + " - line 10, column 3\n" + " - line 11, column 3">>, + <<"Duplicate definitions of double_proto at\n" + " - line 4, column 3\n" + " - line 5, column 3">>, + <<"Duplicate definitions of proto_and_def at\n" + " - line 7, column 3\n" + " - line 8, column 3">>, + <<"Duplicate definitions of put at\n" + " - (builtin location)\n" + " - line 15, column 3">>, + <<"Duplicate definitions of state at\n" + " - (builtin location)\n" + " - line 16, column 3">>]} , {"type_errors", - ["Unbound variable zz at line 17, column 21\n", - "Cannot unify int\n" - " and list(int)\n" - "when checking the application at line 26, column 9 of\n" - " (::) : (int, list(int)) => list(int)\n" - "to arguments\n" - " x : int\n" - " x : int\n", - "Cannot unify string\n" - " and int\n" - "when checking the assignment of the field\n" - " x : map(string, string) (at line 9, column 46)\n" - "to the old value __x and the new value\n" - " __x {[\"foo\"] @ x = x + 1} : map(string, int)\n", - "Cannot unify int\n" - " and string\n" - "when checking the type of the expression at line 34, column 45\n" - " 1 : int\n" - "against the expected type\n" - " string\n", - "Cannot unify string\n" - " and int\n" - "when checking the type of the expression at line 34, column 50\n" - " \"bla\" : string\n" - "against the expected type\n" - " int\n", - "Cannot unify string\n" - " and int\n" - "when checking the type of the expression at line 32, column 18\n" - " \"x\" : string\n" - "against the expected type\n" - " int\n", - "Cannot unify string\n" - " and int\n" - "when checking the type of the expression at line 11, column 56\n" - " \"foo\" : string\n" - "against the expected type\n" - " int\n", - "Cannot unify int\n" - " and string\n" - "when comparing the types of the if-branches\n" - " - w : int (at line 38, column 13)\n" - " - z : string (at line 39, column 10)\n", - "Not a record type: string\n" - "arising from the projection of the field y (at line 22, column 38)\n", - "Not a record type: string\n" - "arising from an assignment of the field y (at line 21, column 42)\n", - "Not a record type: string\n" - "arising from an assignment of the field y (at line 20, column 38)\n", - "Not a record type: string\n" - "arising from an assignment of the field y (at line 19, column 35)\n", - "Ambiguous record type with field y (at line 13, column 25) could be one of\n" - " - r (at line 4, column 10)\n" - " - r' (at line 5, column 10)\n", - "Record type r2 does not have field y (at line 15, column 22)\n", - "The field z is missing when constructing an element of type r2 (at line 15, column 24)\n", - "Repeated name x in pattern\n" - " x :: x (at line 26, column 7)\n", - "No record type with fields y, z (at line 14, column 22)\n"]} + [<<"Unbound variable zz at line 17, column 21">>, + <<"Cannot unify int\n" + " and list(int)\n" + "when checking the application at line 26, column 9 of\n" + " (::) : (int, list(int)) => list(int)\n" + "to arguments\n" + " x : int\n" + " x : int">>, + <<"Cannot unify string\n" + " and int\n" + "when checking the assignment of the field\n" + " x : map(string, string) (at line 9, column 46)\n" + "to the old value __x and the new value\n" + " __x {[\"foo\"] @ x = x + 1} : map(string, int)">>, + <<"Cannot unify int\n" + " and string\n" + "when checking the type of the expression at line 34, column 45\n" + " 1 : int\n" + "against the expected type\n" + " string">>, + <<"Cannot unify string\n" + " and int\n" + "when checking the type of the expression at line 34, column 50\n" + " \"bla\" : string\n" + "against the expected type\n" + " int">>, + <<"Cannot unify string\n" + " and int\n" + "when checking the type of the expression at line 32, column 18\n" + " \"x\" : string\n" + "against the expected type\n" + " int">>, + <<"Cannot unify string\n" + " and int\n" + "when checking the type of the expression at line 11, column 56\n" + " \"foo\" : string\n" + "against the expected type\n" + " int">>, + <<"Cannot unify int\n" + " and string\n" + "when comparing the types of the if-branches\n" + " - w : int (at line 38, column 13)\n" + " - z : string (at line 39, column 10)">>, + <<"Not a record type: string\n" + "arising from the projection of the field y (at line 22, column 38)">>, + <<"Not a record type: string\n" + "arising from an assignment of the field y (at line 21, column 42)">>, + <<"Not a record type: string\n" + "arising from an assignment of the field y (at line 20, column 38)">>, + <<"Not a record type: string\n" + "arising from an assignment of the field y (at line 19, column 35)">>, + <<"Ambiguous record type with field y (at line 13, column 25) could be one of\n" + " - r (at line 4, column 10)\n" + " - r' (at line 5, column 10)">>, + <<"Record type r2 does not have field y (at line 15, column 22)">>, + <<"The field z is missing when constructing an element of type r2 (at line 15, column 24)">>, + <<"Repeated name x in pattern\n" + " x :: x (at line 26, column 7)">>, + <<"No record type with fields y, z (at line 14, column 22)">>]} , {"init_type_error", - ["Cannot unify string\n" - " and map(int, int)\n" - "when checking that 'init' returns a value of type 'state' at line 7, column 3\n"]} + [<<"Cannot unify string\n" + " and map(int, int)\n" + "when checking that 'init' returns a value of type 'state' at line 7, column 3">>]} , {"missing_state_type", - ["Cannot unify string\n" - " and ()\n" - "when checking that 'init' returns a value of type 'state' at line 5, column 3\n"]} + [<<"Cannot unify string\n" + " and ()\n" + "when checking that 'init' returns a value of type 'state' at line 5, column 3">>]} , {"missing_fields_in_record_expression", - ["The field x is missing when constructing an element of type r('a) (at line 7, column 40)\n", - "The field y is missing when constructing an element of type r(int) (at line 8, column 40)\n", - "The fields y, z are missing when constructing an element of type r('1) (at line 6, column 40)\n"]} + [<<"The field x is missing when constructing an element of type r('a) (at line 7, column 40)">>, + <<"The field y is missing when constructing an element of type r(int) (at line 8, column 40)">>, + <<"The fields y, z are missing when constructing an element of type r('1) (at line 6, column 40)">>]} ]. -- 2.30.2