diff --git a/README.md b/README.md index 2592113..ce08545 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,4 @@ can then be loaded into the æternity system. The basic modules for interfacing the compiler: * [aeso_compiler: the Sophia compiler](./docs/aeso_compiler.md) +* [aeso_aci: the ACI interface](./docs/aeso_aci.md) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..227cea2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.0.0 diff --git a/docs/aeso_aci.md b/docs/aeso_aci.md new file mode 100644 index 0000000..082aa6b --- /dev/null +++ b/docs/aeso_aci.md @@ -0,0 +1,135 @@ +# aeso_aci + +### Module + +### aeso_aci + +The ACI interface encoder and decoder. + +### Description + +This module provides an interface to generate and convert between +Sophia contracts and a suitable JSON encoding of contract +interface. As yet the interface is very basic. + +Encoding this contract: + +``` +contract Answers = + record state = { a : answers } + type answers() = map(string, int) + + stateful function init() = { a = {} } + private function the_answer() = 42 + function new_answer(q : string, a : int) : answers() = { [q] = a } +``` + +generates the following JSON structure representing the contract interface: + + +``` json +{ + "contract": { + "name": "Answers", + "type_defs": [ + { + "name": "state", + "vars": [], + "typedef": "{a : map(string,int)}" + }, + { + "name": "answers", + "vars": [], + "typedef": "map(string,int)" + } + ], + "functions": [ + { + "name": "init", + "arguments": [], + "type": "{a : map(string,int)}", + "stateful": true + }, + { + "name": "new_answer", + "arguments": [ + { + "name": "q", + "type": "string" + }, + { + "name": "a", + "type": "int" + } + ], + "type": "map(string,int)", + "stateful": false + } + ] + } +} +``` + +When that encoding is decoded the following include definition is generated: + +``` +contract Answers = + function new_answer : (string, int) => map(string,int) +``` + +### Types +``` erlang +contract_string() = string() | binary() +json_string() = binary() +``` + +### Exports + +#### encode(ContractString) -> {ok,JSONstring} | {error,ErrorString} + +Types + +``` erlang +ConstractString = contract_string() +JSONstring = json_string() +``` + +Generate the JSON encoding of the interface to a contract. The type definitions and non-private functions are included in the JSON string. + +#### decode(JSONstring) -> ConstractString. + +Types + +``` erlang +ConstractString = contract_string() +JSONstring = json_string() +``` + +Take a JSON encoding of a contract interface and generate and generate a contract definition which can be included in another contract. + +### Example run + +This is an example of using the ACI generator from an Erlang shell. The file called `aci_test.aes` contains the contract in the description from which we want to generate files `aci_test.json` which is the JSON encoding of the contract interface and `aci_test.include` which is the contract definition to be included inside another contract. + +``` erlang +1> {ok,Contract} = file:read_file("aci_test.aes"). +{ok,<<"contract Answers =\n record state = { a : answers }\n type answers() = map(string, int)\n\n stateful function"...>>} +2> {ok,Encoding} = aeso_aci:encode(Contract). +<<"{\"contract\":{\"name\":\"Answers\",\"type_defs\":[{\"name\":\"state\",\"vars\":[],\"typedef\":\"{a : map(string,int)}\"},{\"name\":\"ans"...>> +3> file:write_file("aci_test.aci", Encoding). +ok +4> Decoded = aeso_aci:decode(Encoding). +<<"contract Answers =\n function new_answer : (string, int) => map(string,int)\n">> +5> file:write_file("aci_test.include", Decoded). +ok +6> jsx:prettify(Encoding). +<<"{\n \"contract\": {\n \"name\": \"Answers\",\n \"type_defs\": [\n {\n \"name\": \"state\",\n \"vars\": [],\n "...>> +``` + +The final call to `jsx:prettify(Encoding)` returns the encoding in a +more easily readable form. This is what is shown in the description +above. + +### Notes + +The ACI generator currently cannot properly handle types defined using `datatype`. diff --git a/docs/aeso_compiler.md b/docs/aeso_compiler.md index 8bb0f38..5d2e88b 100644 --- a/docs/aeso_compiler.md +++ b/docs/aeso_compiler.md @@ -15,7 +15,7 @@ returns the compiled module in a map which can then be loaded. ``` erlang contract_string() = string() | binary() contract_map() = #{bytecode => binary(), - compiler_version => string(), + compiler_version => binary(), contract_souce => string(), type_info => type_info()} type_info() @@ -75,12 +75,12 @@ Types Get the type representation of a type declaration. -#### version() -> Version +#### version() -> {ok, Version} | {error, term()} Types ``` erlang -Version = integer() +Version = binary() ``` Get the current version of the Sophia compiler. diff --git a/rebar.config b/rebar.config index 1a65d1d..b7b2bf4 100644 --- a/rebar.config +++ b/rebar.config @@ -1,8 +1,12 @@ +%% -*- mode: erlang; indent-tabs-mode: nil -*- + {erl_opts, [debug_info]}. {deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref, "beb6705"}}} , {getopt, "1.0.1"} + , {jsx, {git, "https://github.com/talentdeficit/jsx.git", + {tag, "2.8.0"}}} ]}. {escript_incl_apps, [aesophia, aebytecode, getopt]}. diff --git a/rebar.lock b/rebar.lock index 5720a27..1b415d5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -12,7 +12,11 @@ {ref,"60a335668a60328a29f9731b67c4a0e9e3d50ab6"}}, 2}, {<<"eblake2">>,{pkg,<<"eblake2">>,<<"1.0.0">>},1}, - {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},0}]}. + {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},0}, + {<<"jsx">>, + {git,"https://github.com/talentdeficit/jsx.git", + {ref,"3074d4865b3385a050badf7828ad31490d860df5"}}, + 0}]}. [ {pkg_hash,[ {<<"eblake2">>, <<"EC8AD20E438AAB3F2E8D5D118C366A0754219195F8A0F536587440F8F9BCF2EF">>}, diff --git a/src/aeso_abi.erl b/src/aeso_abi.erl index c9f32ba..d4b371b 100644 --- a/src/aeso_abi.erl +++ b/src/aeso_abi.erl @@ -11,7 +11,7 @@ -define(HASH_SIZE, 32). -export([ old_create_calldata/3 - , create_calldata/5 + , create_calldata/4 , check_calldata/2 , function_type_info/3 , function_type_hash/3 @@ -39,22 +39,12 @@ %%%=================================================================== %%% Handle calldata -create_calldata(Contract, FunName, Args, ArgTypes, RetType) -> - case get_type_info_and_hash(Contract, FunName) of - {ok, TypeInfo, TypeHashInt} -> - Data = aeso_heap:to_binary({TypeHashInt, list_to_tuple(Args)}), - case check_calldata(Data, TypeInfo) of - {ok, CallDataType, OutType} -> - case check_given_type(FunName, ArgTypes, RetType, CallDataType, OutType) of - ok -> - {ok, Data, CallDataType, OutType}; - {error, _} = Err -> - Err - end; - {error,_What} = Err -> Err - end; - {error, _} = Err -> Err - end. +create_calldata(FunName, Args, ArgTypes0, RetType) -> + ArgTypes = {tuple, ArgTypes0}, + <> = + function_type_hash(list_to_binary(FunName), ArgTypes, RetType), + Data = aeso_heap:to_binary({TypeHashInt, list_to_tuple(Args)}), + {ok, Data, {tuple, [word, ArgTypes]}, RetType}. get_type_info_and_hash(#{type_info := TypeInfo}, FunName) -> FunBin = list_to_binary(FunName), @@ -64,26 +54,6 @@ get_type_info_and_hash(#{type_info := TypeInfo}, FunName) -> {error, _} = Err -> Err end. -%% Check that the given type matches the type from the metadata. -check_given_type(FunName, GivenArgs, GivenRet, CalldataType, ExpectRet) -> - {tuple, [word, {tuple, ExpectArgs}]} = CalldataType, - ReturnOk = if FunName == "init" -> true; - GivenRet == any -> true; - true -> GivenRet == ExpectRet - end, - ArgsOk = ExpectArgs == GivenArgs, - case ReturnOk andalso ArgsOk of - true -> ok; - false when FunName == "init" -> - {error, {init_args_mismatch, - {given, GivenArgs}, - {expected, ExpectArgs}}}; - false -> - {error, {call_type_mismatch, - {given, GivenArgs, '=>', GivenRet}, - {expected, ExpectArgs, '=>', ExpectRet}}} - end. - -spec check_calldata(binary(), type_info()) -> {'ok', typerep(), typerep()} | {'error', atom()}. check_calldata(CallData, TypeInfo) -> @@ -121,8 +91,8 @@ get_function_hash_from_calldata(CallData) -> -spec function_type_info(function_name(), [typerep()], typerep()) -> function_type_info(). -function_type_info(Name, Args, OutType) -> - ArgType = {tuple, [T || {_, T} <- Args]}, +function_type_info(Name, ArgTypes, OutType) -> + ArgType = {tuple, ArgTypes}, { function_type_hash(Name, ArgType, OutType) , Name , aeso_heap:to_binary(ArgType) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl new file mode 100644 index 0000000..0f7d38c --- /dev/null +++ b/src/aeso_aci.erl @@ -0,0 +1,276 @@ +%%%------------------------------------------------------------------- +%%% @author Robert Virding +%%% @copyright (C) 2017, Aeternity Anstalt +%%% @doc +%%% ACI interface +%%% @end +%%% Created : 12 Dec 2017 +%%%------------------------------------------------------------------- + +-module(aeso_aci). + +-export([encode/1,encode/2,decode/1]). + +%% Define records for the various typed syntactic forms. These make +%% the code easier but don't seem to exist elsewhere. + +-record(contract, {ann,con,decls}). +-record(namespace, {ann,con,decls}). +-record(letfun, {ann,id,args,type,body}). +-record(type_def, {ann,id,vars,typedef}). + +-record(app_t, {ann,id,fields}). +-record(tuple_t, {ann,args}). +-record(record_t, {fields}). +-record(field_t, {ann,id,type}). +-record(alias_t, {type}). +-record(variant_t, {cons}). +-record(constr_t, {ann,con,args}). +-record(fun_t, {ann,named,args,type}). + +-record(arg, {ann,id,type}). +-record(id, {ann,name}). +-record(con, {ann,name}). +-record(qid, {ann,names}). +-record(qcon, {ann,names}). +-record(tvar, {ann,name}). + +%% encode(ContractString) -> {ok,JSON} | {error,String}. +%% encode(ContractString, Options) -> {ok,JSON} | {error,String}. +%% Build a JSON structure with lists and tuples, not maps, as this +%% allows us to order the fields in the contructed JSON string. + +encode(ContractString) -> encode(ContractString, []). + +encode(ContractString, Options) when is_binary(ContractString) -> + encode(binary_to_list(ContractString), Options); +encode(ContractString, Options) -> + try + Ast = parse(ContractString, Options), + %%io:format("~p\n", [Ast]), + %% aeso_ast:pp(Ast), + TypedAst = aeso_ast_infer_types:infer(Ast, Options), + %% io:format("~p\n", [TypedAst]), + %% aeso_ast:pp_typed(TypedAst), + %% We find and look at the last contract. + Contract = lists:last(TypedAst), + Cname = contract_name(Contract), + Tdefs = [ encode_typedef(T) || + T <- sort_decls(contract_types(Contract)) ], + Fdefs = [ encode_func(F) || F <- sort_decls(contract_funcs(Contract)), + not is_private_func(F) ], + Jmap = [{<<"contract">>, [{<<"name">>, list_to_binary(Cname)}, + {<<"type_defs">>, Tdefs}, + {<<"functions">>, Fdefs}]}], + %% io:format("~p\n", [Jmap]), + {ok,jsx:encode(Jmap)} + 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")). + +encode_func(Fdef) -> + Name = function_name(Fdef), + Args = function_args(Fdef), + Type = function_type(Fdef), + [{<<"name">>, list_to_binary(Name)}, + {<<"arguments">>, encode_args(Args)}, + {<<"type">>, list_to_binary(encode_type(Type))}, + {<<"stateful">>, is_stateful_func(Fdef)}]. + +encode_args(Args) -> + [ encode_arg(A) || A <- Args ]. + +encode_arg(#arg{id=Id,type=T}) -> + [{<<"name">>,list_to_binary(encode_type(Id))}, + {<<"type">>,list_to_binary(encode_type(T))}]. + +encode_types(Types) -> + [ encode_type(T) || T <- Types ]. + +encode_type(#tvar{name=N}) -> N; +encode_type(#id{name=N}) -> N; +encode_type(#con{name=N}) -> N; +encode_type(#qid{names=Ns}) -> + lists:join(".", Ns); +encode_type(#qcon{names=Ns}) -> + lists:join(".", Ns); %? +encode_type(#tuple_t{args=As}) -> + Eas = encode_types(As), + [$(,lists:join(",", Eas),$)]; +encode_type(#record_t{fields=Fs}) -> + Efs = encode_types(Fs), + [${,lists:join(",", Efs),$}]; +encode_type(#app_t{id=Id,fields=Fs}) -> + Name = encode_type(Id), + Efs = encode_types(Fs), + [Name,"(",lists:join(",", Efs),")"]; +encode_type(#field_t{id=Id,type=T}) -> + [encode_type(Id)," : ",encode_type(T)]; +encode_type(#variant_t{cons=Cs}) -> + Ecs = encode_types(Cs), + lists:join(" | ", Ecs); +encode_type(#constr_t{con=C,args=As}) -> + Ec = encode_type(C), + Eas = encode_types(As), + [Ec,$(,lists:join(", ", Eas),$)]; +encode_type(#fun_t{args=As,type=T}) -> + Eas = encode_types(As), + Et = encode_type(T), + [$(,lists:join(", ", Eas),") => ",Et]. + +encode_typedef(Type) -> + Name = typedef_name(Type), + Vars = typedef_vars(Type), + Def = typedef_def(Type), + [{<<"name">>, list_to_binary(Name)}, + {<<"vars">>, encode_tvars(Vars)}, + {<<"typedef">>, list_to_binary(encode_alias(Def))}]. + +encode_tvars(Vars) -> + [ encode_tvar(V) || V <- Vars ]. + +encode_tvar(#tvar{name=N}) -> + [{<<"name">>, list_to_binary(N)}]. + +encode_alias(#alias_t{type=T}) -> + encode_type(T); +encode_alias(A) -> encode_type(A). + +%% decode(JSON) -> ContractString. +%% Decode a JSON string and generate a suitable contract string which +%% can be included in a contract definition. We decode into a map +%% here as this is easier to work with and order is not important. + +decode(Json) -> + Map = jsx:decode(Json, [return_maps]), + %% io:format("~p\n", [Map]), + #{<<"contract">> := C} = Map, + list_to_binary(decode_contract(C)). + +decode_contract(#{<<"name">> := Name, + <<"type_defs">> := _Ts, + <<"functions">> := Fs}) -> + ["contract"," ",io_lib:format("~s", [Name])," =\n", + [], %Don't include types yet. + %% decode_tdefs(Ts), + decode_funcs(Fs)]. + +decode_funcs(Fs) -> [ decode_func(F) || F <- Fs ]. + +decode_func(#{<<"name">> := <<"init">>}) -> []; +decode_func(#{<<"name">> := Name,<<"arguments">> := As,<<"type">> := T}) -> + [" function"," ",io_lib:format("~s", [Name])," : ", + decode_args(As)," => ",decode_type(T),$\n]. + +decode_type(T) -> io_lib:format("~s", [T]). + +decode_args(As) -> + Das = [ decode_arg(A) || A <- As ], + [$(,lists:join(", ", Das),$)]. + +decode_arg(#{<<"type">> := T}) -> decode_type(T). + +%% To keep dialyzer happy and quiet. +%% decode_tdefs(Ts) -> [ decode_tdef(T) || T <- Ts ]. +%% +%% decode_tdef(#{<<"name">> := Name,<<"vars">> := Vs,<<"typedef">> := T}) -> +%% [" type"," ",io_lib:format("~s", [Name]),decode_tvars(Vs), +%% " = ",decode_type(T),$\n]. +%% +%% decode_tvars([]) -> []; %No tvars, no parentheses +%% decode_tvars(Vs) -> +%% Dvs = [ decode_tvar(V) || V <- Vs ], +%% [$(,lists:join(", ", Dvs),$)]. +%% +%% decode_tvar(#{<<"name">> := N}) -> io_lib:format("~s", [N]). +%% +%% #contract{Ann, Con, [Declarations]}. + +contract_name(#contract{con=#con{name=N}}) -> N. + +contract_funcs(#contract{decls=Decls}) -> + [ D || D <- Decls, is_record(D, letfun) ]. + +contract_types(#contract{decls=Decls}) -> + [ D || D <- Decls, is_record(D, type_def) ]. + +%% To keep dialyzer happy and quiet. +%% namespace_name(#namespace{con=#con{name=N}}) -> N. +%% +%% namespace_funcs(#namespace{decls=Decls}) -> +%% [ D || D <- Decls, is_record(D, letfun) ]. +%% +%% namespace_types(#namespace{decls=Decls}) -> +%% [ D || D <- Decls, is_record(D, type_def) ]. + +sort_decls(Ds) -> + Sort = fun (D1, D2) -> + aeso_syntax:get_ann(line, D1, 0) =< + aeso_syntax:get_ann(line, D2, 0) + end, + lists:sort(Sort, Ds). + +%% #letfun{Ann, Id, [Arg], Type, Typedef}. + +function_name(#letfun{id=#id{name=N}}) -> N. + +function_args(#letfun{args=Args}) -> Args. + +function_type(#letfun{type=Type}) -> Type. + +is_private_func(#letfun{ann=A}) -> aeso_syntax:get_ann(private, A, false). + +is_stateful_func(#letfun{ann=A}) -> aeso_syntax:get_ann(stateful, A, false). + +%% #type_def{Ann, Id, [Var], Typedef}. + +typedef_name(#type_def{id=#id{name=N}}) -> N. + +typedef_vars(#type_def{vars=Vars}) -> Vars. + +typedef_def(#type_def{typedef=Def}) -> Def. + +parse(Text, Options) -> + %% Try and return something sensible here! + case aeso_parser:string(Text, Options) of + %% Yay, it worked! + {ok, Contract} -> Contract; + %% Scan errors. + {error, {Pos, scan_error}} -> + parse_error(Pos, "scan error"); + {error, {Pos, scan_error_no_state}} -> + parse_error(Pos, "scan error"); + %% Parse errors. + {error, {Pos, parse_error, Error}} -> + parse_error(Pos, Error); + {error, {Pos, ambiguous_parse, As}} -> + ErrorString = io_lib:format("Ambiguous ~p", [As]), + parse_error(Pos, ErrorString); + %% Include error + {error, {Pos, include_error, File}} -> + parse_error(Pos, io_lib:format("could not find include file '~s'", [File])) + end. + +parse_error(Pos, ErrorString) -> + io:format("Error ~p ~p\n", [Pos,ErrorString]), + Error = io_lib:format("~s: ~s", [pos_error(Pos), ErrorString]), + error({parse_errors, [Error]}). + +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_ast.erl b/src/aeso_ast.erl index 90e6595..4c36123 100644 --- a/src/aeso_ast.erl +++ b/src/aeso_ast.erl @@ -17,7 +17,6 @@ line({symbol, Line, _}) -> Line. symbol_name({symbol, _, Name}) -> Name. pp(Ast) -> - %% io:format("Tree:\n~p\n",[Ast]), String = prettypr:format(aeso_pretty:decls(Ast, [])), io:format("Ast:\n~s\n", [String]). diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 8eb049a..c247597 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -12,7 +12,7 @@ -module(aeso_ast_infer_types). --export([infer/1, infer/2, infer_constant/1]). +-export([infer/1, infer/2, infer_constant/1, unfold_types_in_type/3]). -type utype() :: {fun_t, aeso_syntax:ann(), named_args_t(), [utype()], utype()} | {app_t, aeso_syntax:ann(), utype(), [utype()]} @@ -491,37 +491,29 @@ map_t(As, K, V) -> {app_t, As, {id, As, "map"}, [K, V]}. infer(Contracts) -> infer(Contracts, []). --type option() :: permissive_address_literals. +-type option() :: permissive_address_literals | return_env. -spec init_env(list(option())) -> env(). -init_env(Options) -> - case proplists:get_value(permissive_address_literals, Options, false) of - false -> global_env(); - true -> - %% Treat oracle and query ids as address to allow address literals for these - Ann = [{origin, system}], - Tag = fun(Tag, Val) -> {Tag, Ann, Val} end, - lists:foldl(fun({Name, Arity}, E) -> - bind_type(Name, [{origin, system}], - {lists:duplicate(Arity, Tag(tvar, "_")), - {alias_t, Tag(id, "address")}}, E) - end, global_env(), [{"oracle", 2}, {"oracle_query", 2}]) - end. +init_env(_Options) -> global_env(). --spec infer(aeso_syntax:ast(), list(option())) -> aeso_syntax:ast(). +-spec infer(aeso_syntax:ast(), list(option())) -> aeso_syntax:ast() | {env(), aeso_syntax:ast()}. infer(Contracts, Options) -> ets_init(), %% Init the ETS table state try Env = init_env(Options), create_options(Options), ets_new(type_vars, [set]), - infer1(Env, Contracts, []) + {Env1, Decls} = infer1(Env, Contracts, []), + case proplists:get_value(return_env, Options, false) of + false -> Decls; + true -> {Env1, Decls} + end after clean_up_ets() end. --spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()]) -> [aeso_syntax:decl()]. -infer1(_, [], Acc) -> lists:reverse(Acc); +-spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()]) -> {env(), [aeso_syntax:decl()]}. +infer1(Env, [], Acc) -> {Env, lists:reverse(Acc)}; infer1(Env, [{contract, Ann, ConName, Code} | Rest], Acc) -> %% do type inference on each contract independently. check_scope_name_clash(Env, contract, ConName), @@ -1548,7 +1540,8 @@ unfold_types_in_type(Env, T) -> unfold_types_in_type(Env, T, []). unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) -> - UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), + UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), + UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false), case lookup_type(Env, Id) of {_, {_, {Formals, {record_t, Fields}}}} when UnfoldRecords, length(Formals) == length(Args) -> {record_t, @@ -1556,6 +1549,11 @@ unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) subst_tvars(lists:zip(Formals, Args), Fields), Options)}; {_, {_, {Formals, {alias_t, Type}}}} when length(Formals) == length(Args) -> unfold_types_in_type(Env, subst_tvars(lists:zip(Formals, Args), Type), Options); + {_, {_, {Formals, {variant_t, Constrs}}}} when UnfoldVariants, length(Formals) == length(Args) -> + %% TODO: unfolding variant types will not work well if we add recursive types! + {variant_t, + unfold_types_in_type(Env, + subst_tvars(lists:zip(Formals, Args), Constrs), Options)}; _ -> %% Not a record type, or ill-formed record type. {app_t, Ann, Id, unfold_types_in_type(Env, Args, Options)} @@ -1563,9 +1561,12 @@ unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) -> %% Like the case above, but for types without parameters. UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), + UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false), case lookup_type(Env, Id) of {_, {_, {[], {record_t, Fields}}}} when UnfoldRecords -> {record_t, unfold_types_in_type(Env, Fields, Options)}; + {_, {_, {[], {variant_t, Constrs}}}} when UnfoldVariants -> + {variant_t, unfold_types_in_type(Env, Constrs, Options)}; {_, {_, {[], {alias_t, Type1}}}} -> unfold_types_in_type(Env, Type1, Options); _ -> @@ -1574,6 +1575,8 @@ unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) -> end; unfold_types_in_type(Env, {field_t, Attr, Name, Type}, Options) -> {field_t, Attr, Name, unfold_types_in_type(Env, Type, Options)}; +unfold_types_in_type(Env, {constr_t, Ann, Con, Types}, Options) -> + {constr_t, Ann, Con, unfold_types_in_type(Env, Types, Options)}; unfold_types_in_type(Env, T, Options) when is_tuple(T) -> list_to_tuple(unfold_types_in_type(Env, tuple_to_list(T), Options)); unfold_types_in_type(Env, [H|T], Options) -> @@ -1652,9 +1655,16 @@ unify1(_Env, A, B, When) -> Kind = fun({qcon, _, _}) -> con; ({con, _, _}) -> con; ({id, _, "address"}) -> addr; + ({id, _, "hash"}) -> hash; + ({app_t, _, {id, _, "oracle"}, _}) -> oracle; + ({app_t, _, {id, _, "oracle_query"}, _}) -> query; (_) -> other end, - %% If permissive_address_literals we allow unifying contract types and address - [addr, con] == lists:usort([Kind(A), Kind(B)]); + %% If permissive_address_literals we allow unifying adresses + %% with contract types or oracles/oracle queries + case lists:usort([Kind(A), Kind(B)]) of + [addr, K] -> K /= other; + _ -> false + end; false -> false end, [ cannot_unify(A, B, When) || not Ok ], @@ -1849,7 +1859,7 @@ pp_error({event_0_to_3_indexed_values, Constr}) -> io_lib:format("The event constructor ~s (at ~s) has too many indexed values (max 3)\n", [name(Constr), pp_loc(Constr)]); pp_error({event_0_to_1_string_values, Constr}) -> - io_lib:format("The event constructor ~s (at ~s) has too many string values (max 1)\n", + io_lib:format("The event constructor ~s (at ~s) has too many non-indexed values (max 1)\n", [name(Constr), pp_loc(Constr)]); pp_error({repeated_constructor, Cs}) -> io_lib:format("Variant types must have distinct constructor names\n~s", diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 7a82115..6561eb8 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -11,10 +11,12 @@ -export([ file/1 , file/2 , from_string/2 - , check_call/2 + , check_call/4 , create_calldata/3 , version/0 , sophia_type_to_typerep/1 + , to_sophia_value/4 + , to_sophia_value/5 ]). -include_lib("aebytecode/include/aeb_opcodes.hrl"). @@ -37,14 +39,24 @@ , options/0 ]). --define(COMPILER_VERSION_1, 1). --define(COMPILER_VERSION_2, 2). - --define(COMPILER_VERSION, ?COMPILER_VERSION_2). - --spec version() -> pos_integer(). +-spec version() -> {ok, binary()} | {error, term()}. version() -> - ?COMPILER_VERSION. + case lists:keyfind(aesophia, 1, application:loaded_applications()) of + false -> + case application:load(aesophia) of + ok -> + case application:get_key(aesophia, vsn) of + {ok, VsnString} -> + {ok, list_to_binary(VsnString)}; + undefined -> + {error, failed_to_load_aesophia} + end; + Err = {error, _} -> + Err + end; + {_App, _Des, VsnString} -> + {ok, list_to_binary(VsnString)} + end. -spec file(string()) -> {ok, map()} | {error, binary()}. file(Filename) -> @@ -66,22 +78,16 @@ from_string(ContractBin, Options) when is_binary(ContractBin) -> from_string(binary_to_list(ContractBin), Options); from_string(ContractString, Options) -> 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), + #{icode := Icode} = string_to_icode(ContractString, Options), + TypeInfo = extract_type_info(Icode), + Assembler = assemble(Icode, Options), + pp_assembler(Assembler, Options), ByteCodeList = to_bytecode(Assembler, Options), ByteCode = << << B:8 >> || B <- ByteCodeList >>, - ok = pp_bytecode(ByteCode, Options), + pp_bytecode(ByteCode, Options), + {ok, Version} = version(), {ok, #{byte_code => ByteCode, - compiler_version => version(), + compiler_version => Version, contract_source => ContractString, type_info => TypeInfo }} @@ -97,38 +103,69 @@ from_string(ContractString, Options) -> %% General programming errors in the compiler just signal error. end. +-spec string_to_icode(string(), [option() | permissive_address_literals]) -> map(). +string_to_icode(ContractString, Options0) -> + {InferOptions, Options} = lists:partition(fun(Opt) -> Opt == permissive_address_literals end, Options0), + Ast = parse(ContractString, Options), + pp_sophia_code(Ast, Options), + pp_ast(Ast, Options), + {TypeEnv, TypedAst} = aeso_ast_infer_types:infer(Ast, [return_env | InferOptions]), + pp_typed_ast(TypedAst, Options), + Icode = ast_to_icode(TypedAst, Options), + pp_icode(Icode, Options), + #{ typed_ast => TypedAst, + type_env => TypeEnv, + icode => Icode }. + join_errors(Prefix, Errors, Pfun) -> Ess = [ Pfun(E) || E <- Errors ], list_to_binary(string:join([Prefix|Ess], "\n")). --define(CALL_NAME, "__call"). +-define(CALL_NAME, "__call"). +-define(DECODE_NAME, "__decode"). %% Takes a string containing a contract with a declaration/prototype of a -%% function (foo, say) and a function __call() = foo(args) calling this +%% function (foo, say) and adds function __call() = foo(args) calling this %% function. Returns the name of the called functions, typereps and Erlang %% terms for the arguments. --spec check_call(string(), options()) -> {ok, string(), {[Type], Type | any}, [term()]} | {error, term()} +%% NOTE: Special treatment for "init" since it might be implicit and has +%% a special return type (typerep, T) +-spec check_call(string(), string(), [string()], options()) -> {ok, string(), {[Type], Type}, [term()]} | {error, term()} when Type :: term(). -check_call(ContractString, Options) -> +check_call(Source, "init" = FunName, Args, Options) -> + PatchFun = fun(T) -> {tuple, [typerep, T]} end, + case check_call(Source, FunName, Args, Options, PatchFun) of + Err = {error, _} when Args == [] -> + %% Try with default init-function + case check_call(insert_init_function(Source, Options), FunName, Args, Options, PatchFun) of + {error, _} -> Err; %% The first error is most likely better... + Res -> Res + end; + Res -> + Res + end; +check_call(Source, FunName, Args, Options) -> + PatchFun = fun(T) -> T end, + check_call(Source, FunName, Args, Options, PatchFun). + +check_call(ContractString0, FunName, Args, Options, PatchFun) -> 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]), + %% First check the contract without the __call function and no permissive literals + #{} = string_to_icode(ContractString0, Options), + ContractString = insert_call_function(ContractString0, FunName, Args, Options), + #{typed_ast := TypedAst, + icode := Icode} = string_to_icode(ContractString, [permissive_address_literals | Options]), {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} + {ok, FunName, {ArgVMTypes, PatchFun(RetVMType)}, ArgTerms} catch error:{parse_errors, Errors} -> {error, join_errors("Parse errors", Errors, fun (E) -> E end)}; @@ -142,33 +179,146 @@ check_call(ContractString, Options) -> fun (E) -> io_lib:format("~p", [E]) end)} end. --spec create_calldata(map(), string(), string()) -> - {ok, binary(), aeso_sophia:type(), aeso_sophia:type()} - | {error, argument_syntax_error}. -create_calldata(Contract, "", CallCode) when is_map(Contract) -> - case check_call(CallCode, []) of - {ok, FunName, {ArgTypes, RetType}, Args} -> - aeso_abi:create_calldata(Contract, FunName, Args, ArgTypes, RetType); - {error, _} = Err -> Err - end; -create_calldata(Contract, Function, Argument) when is_map(Contract) -> - %% Slightly hacky shortcut to let you get away without writing the full - %% call contract code. - %% 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 - [FunName] -> aeso_abi:old_create_calldata(Contract, FunName, Argument); - [FunName | _] -> - Args = lists:map(fun($\n) -> 32; (X) -> X end, Argument), %% newline to space - CallContract = lists:flatten( - [ "contract MakeCall =\n" - , " function ", Function, "\n" - , " function __call() = ", FunName, "(", Args, ")" - ]), - create_calldata(Contract, "", CallContract) +%% Add the __call function to a contract. +-spec insert_call_function(string(), string(), [string()], options()) -> string(). +insert_call_function(Code, FunName, Args, Options) -> + Ast = parse(Code, Options), + Ind = last_contract_indent(Ast), + lists:flatten( + [ Code, + "\n\n", + lists:duplicate(Ind, " "), + "function __call() = ", FunName, "(", string:join(Args, ","), ")\n" + ]). + +-spec insert_init_function(string(), options()) -> string(). +insert_init_function(Code, Options) -> + Ast = parse(Code, Options), + Ind = last_contract_indent(Ast), + lists:flatten( + [ Code, + "\n\n", + lists:duplicate(Ind, " "), "function init() = ()\n" + ]). + +last_contract_indent(Decls) -> + case lists:last(Decls) of + {_, _, _, [Decl | _]} -> aeso_syntax:get_ann(col, Decl, 1) - 1; + _ -> 0 end. +-spec to_sophia_value(string(), string(), ok | error | revert, aeso_sophia:data()) -> + {ok, aeso_syntax:expr()} | {error, term()}. +to_sophia_value(ContractString, Fun, ResType, Data) -> + to_sophia_value(ContractString, Fun, ResType, Data, []). + +-spec to_sophia_value(string(), string(), ok | error | revert, binary(), options()) -> + {ok, aeso_syntax:expr()} | {error, term()}. +to_sophia_value(_, _, error, Err, _Options) -> + {ok, {app, [], {id, [], "error"}, [{string, [], Err}]}}; +to_sophia_value(_, _, revert, Data, _Options) -> + case aeso_heap:from_binary(string, Data) of + {ok, Err} -> {ok, {app, [], {id, [], "abort"}, [{string, [], Err}]}}; + {error, _} = Err -> Err + end; +to_sophia_value(ContractString, FunName, ok, Data, Options) -> + try + #{ typed_ast := TypedAst, + type_env := TypeEnv, + icode := Icode } = string_to_icode(ContractString, Options), + {ok, Type0} = get_decode_type(FunName, TypedAst), + Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, [unfold_record_types, unfold_variant_types]), + VmType = aeso_ast_to_icode:ast_typerep(Type, Icode), + case aeso_heap:from_binary(VmType, Data) of + {ok, VmValue} -> + try + {ok, translate_vm_value(VmType, Type, VmValue)} + catch throw:cannot_translate_to_sophia -> + Type0Str = prettypr:format(aeso_pretty:type(Type0)), + {error, join_errors("Translation error", [lists:flatten(io_lib:format("Cannot translate VM value ~p\n of type ~p\n to Sophia type ~s\n", + [Data, VmType, Type0Str]))], + fun (E) -> E end)} + end; + {error, _Err} -> + {error, join_errors("Decode errors", [lists:flatten(io_lib:format("Failed to decode binary at type ~p", [VmType]))], + fun(E) -> E end)} + end + 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_function}} -> + {error, join_errors("Type errors", ["no function: '" ++ FunName ++ "'"], + fun (E) -> E end)}; + throw:Error -> %Don't ask + {error, join_errors("Code errors", [Error], + fun (E) -> io_lib:format("~p", [E]) end)} + end. + +address_literal(N) -> {hash, [], <>}. % TODO + +%% TODO: somewhere else +translate_vm_value(word, {id, _, "address"}, N) -> address_literal(N); +translate_vm_value(word, {app_t, _, {id, _, "oracle"}, _}, N) -> address_literal(N); +translate_vm_value(word, {app_t, _, {id, _, "oracle_query"}, _}, N) -> address_literal(N); +translate_vm_value(word, {id, _, "hash"}, N) -> {hash, [], <>}; +translate_vm_value(word, {id, _, "int"}, N) -> {int, [], N}; +translate_vm_value(word, {id, _, "bits"}, N) -> error({todo, bits, N}); +translate_vm_value(word, {id, _, "bool"}, N) -> {bool, [], N /= 0}; +translate_vm_value({tuple, [word, word]}, {id, _, "signature"}, {tuple, [Hi, Lo]}) -> + {hash, [], <>}; +translate_vm_value(string, {id, _, "string"}, S) -> {string, [], S}; +translate_vm_value({list, VmType}, {app_t, _, {id, _, "list"}, [Type]}, List) -> + {list, [], [translate_vm_value(VmType, Type, X) || X <- List]}; +translate_vm_value({option, VmType}, {app_t, _, {id, _, "option"}, [Type]}, Val) -> + case Val of + none -> {con, [], "None"}; + {some, X} -> {app, [], {con, [], "Some"}, [translate_vm_value(VmType, Type, X)]} + end; +translate_vm_value({variant, [[], [VmType]]}, {app_t, _, {id, _, "option"}, [Type]}, Val) -> + case Val of + {variant, 0, []} -> {con, [], "None"}; + {variant, 1, [X]} -> {app, [], {con, [], "Some"}, [translate_vm_value(VmType, Type, X)]} + end; +translate_vm_value({tuple, VmTypes}, {tuple_t, _, Types}, Val) + when length(VmTypes) == length(Types), + length(VmTypes) == tuple_size(Val) -> + {tuple, [], [translate_vm_value(VmType, Type, X) + || {VmType, Type, X} <- lists:zip3(VmTypes, Types, tuple_to_list(Val))]}; +translate_vm_value({tuple, VmTypes}, {record_t, Fields}, Val) + when length(VmTypes) == length(Fields), + length(VmTypes) == tuple_size(Val) -> + {record, [], [ {field, [], [{proj, [], FName}], translate_vm_value(VmType, FType, X)} + || {VmType, {field_t, _, FName, FType}, X} <- lists:zip3(VmTypes, Fields, tuple_to_list(Val)) ]}; +translate_vm_value({map, VmKeyType, VmValType}, {app_t, _, {id, _, "map"}, [KeyType, ValType]}, Map) + when is_map(Map) -> + {map, [], [ {translate_vm_value(VmKeyType, KeyType, Key), + translate_vm_value(VmValType, ValType, Val)} + || {Key, Val} <- maps:to_list(Map) ]}; +translate_vm_value({variant, VmCons}, {variant_t, Cons}, {variant, Tag, Args}) + when length(VmCons) == length(Cons), + length(VmCons) > Tag -> + VmTypes = lists:nth(Tag + 1, VmCons), + ConType = lists:nth(Tag + 1, Cons), + translate_vm_value(VmTypes, ConType, Args); +translate_vm_value(VmTypes, {constr_t, _, Con, Types}, Args) + when length(VmTypes) == length(Types), + length(VmTypes) == length(Args) -> + {app, [], Con, [ translate_vm_value(VmType, Type, Arg) + || {VmType, Type, Arg} <- lists:zip3(VmTypes, Types, Args) ]}; +translate_vm_value(_VmType, _Type, _Data) -> + throw(cannot_translate_to_sophia). + +-spec create_calldata(string(), string(), [string()]) -> + {ok, binary(), aeso_sophia:type(), aeso_sophia:type()} + | {error, term()}. +create_calldata(Code, Fun, Args) -> + case check_call(Code, Fun, Args, []) of + {ok, FunName, {ArgTypes, RetType}, VMArgs} -> + aeso_abi:create_calldata(FunName, VMArgs, ArgTypes, RetType); + {error, _} = Err -> Err + end. get_arg_icode(Funs) -> case [ Args || {[_, ?CALL_NAME], _, _, {funcall, _, Args}, _} <- Funs ] of @@ -189,6 +339,18 @@ get_call_type([_ | Contracts]) -> %% The __call should be in the final contract get_call_type(Contracts). +get_decode_type(FunName, [{contract, _, _, Defs}]) -> + GetType = fun({letfun, _, {id, _, Name}, _, Ret, _}) when Name == FunName -> [Ret]; + ({fun_decl, _, {id, _, Name}, {fun_t, _, _, _, Ret}}) when Name == FunName -> [Ret]; + (_) -> [] end, + case lists:flatmap(GetType, Defs) of + [Type] -> {ok, Type}; + [] -> {error, missing_function} + end; +get_decode_type(FunName, [_ | Contracts]) -> + %% The __decode should be in the final contract + get_decode_type(FunName, Contracts). + %% Translate an icode value (error if not value) to an Erlang term that can be %% consumed by aeso_heap:to_binary(). icode_to_term(word, {integer, N}) -> N; @@ -223,7 +385,7 @@ icode_to_term(T, V) -> icodes_to_terms(Ts, Vs) -> [ icode_to_term(T, V) || {T, V} <- lists:zip(Ts, Vs) ]. -to_icode(TypedAst, Options) -> +ast_to_icode(TypedAst, Options) -> aeso_ast_to_icode:convert_typed(TypedAst, Options). assemble(Icode, Options) -> @@ -237,7 +399,9 @@ to_bytecode([Op|Rest], Options) -> to_bytecode([], _) -> []. extract_type_info(#{functions := Functions} =_Icode) -> - TypeInfo = [aeso_abi:function_type_info(list_to_binary(lists:last(Name)), Args, TypeRep) + ArgTypesOnly = fun(As) -> [ T || {_, T} <- As ] end, + TypeInfo = [aeso_abi:function_type_info(list_to_binary(lists:last(Name)), + ArgTypesOnly(Args), TypeRep) || {Name, Attrs, Args,_Body, TypeRep} <- Functions, not is_tuple(Name), not lists:member(private, Attrs) diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index b934e08..78b329f 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -68,9 +68,14 @@ decl() -> modifiers() -> many(choice([token(stateful), token(public), token(private), token(internal)])). -add_modifiers(Mods, Node) -> - lists:foldl(fun({Mod, _}, X) -> set_ann(Mod, true, X) end, - Node, Mods). +add_modifiers([], Node) -> Node; +add_modifiers(Mods = [Tok | _], Node) -> + %% Set the position to the position of the first modifier. This is + %% important for code transformation tools (like what we do in + %% create_calldata) to be able to get the indentation of the declaration. + set_pos(get_pos(Tok), + lists:foldl(fun({Mod, _}, X) -> set_ann(Mod, true, X) end, + Node, Mods)). %% -- Type declarations ------------------------------------------------------ diff --git a/src/aeso_pretty.erl b/src/aeso_pretty.erl index 8e90693..240c499 100644 --- a/src/aeso_pretty.erl +++ b/src/aeso_pretty.erl @@ -307,6 +307,8 @@ expr_p(P, E = {app, _, F = {Op, _}, Args}) when is_atom(Op) -> {prefix, [A]} -> prefix(P, Op, A); _ -> app(P, F, Args) end; +expr_p(_, {app, _, C={Tag, _, _}, []}) when Tag == con; Tag == qcon -> + expr_p(0, C); expr_p(P, {app, _, F, Args}) -> app(P, F, Args); %% -- Constants @@ -318,6 +320,7 @@ expr_p(_, E = {int, _, N}) -> text(S); expr_p(_, {bool, _, B}) -> text(atom_to_list(B)); expr_p(_, {hash, _, <>}) -> text("#" ++ integer_to_list(N, 16)); +expr_p(_, {hash, _, <>}) -> text("#" ++ integer_to_list(N, 16)); expr_p(_, {unit, _}) -> text("()"); expr_p(_, {string, _, S}) -> term(binary_to_list(S)); expr_p(_, {char, _, C}) -> diff --git a/src/aesophia.app.src b/src/aesophia.app.src index 051642b..3957c45 100644 --- a/src/aesophia.app.src +++ b/src/aesophia.app.src @@ -1,10 +1,11 @@ {application, aesophia, [{description, "Contract Language for aeternity"}, - {vsn, "2.0.0"}, + {vsn, {cmd, "cat VERSION | tr -d '[:space:]'"}}, {registered, []}, {applications, [kernel, stdlib, + jsx, syntax_tools, getopt, aebytecode diff --git a/src/aesophia.erl b/src/aesophia.erl index c7c57d9..277b444 100644 --- a/src/aesophia.erl +++ b/src/aesophia.erl @@ -4,6 +4,7 @@ -define(OPT_SPEC, [ {src_file, undefined, undefined, string, "Sophia source code file"} + , {version, $V, "version", undefined, "Print compiler version"} , {verbose, $v, "verbose", undefined, "Verbose output"} , {help, $h, "help", undefined, "Show this message"} , {outfile, $o, "out", string, "Output file (experimental)"} ]). @@ -14,11 +15,13 @@ usage() -> main(Args) -> case getopt:parse(?OPT_SPEC, Args) of {ok, {Opts, []}} -> - case proplists:get_value(help, Opts, false) of - false -> - compile(Opts); - true -> - usage() + case Opts of + [version] -> + print_vsn(); + [help] -> + usage(); + _ -> + compile(Opts) end; {ok, {_, NonOpts}} -> @@ -69,3 +72,7 @@ write_outfile(Out, ResMap) -> %% Lazy approach file:write_file(Out, term_to_binary(ResMap)), io:format("Output written to: ~s\n", [Out]). + +print_vsn() -> + {ok, Vsn} = aeso_compiler:version(), + io:format("Compiler version: ~s\n", [Vsn]). diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index c5f507c..fd0273f 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -54,27 +54,115 @@ encode_decode_test() -> ok. encode_decode_sophia_test() -> - {42} = encode_decode_sophia_string("int", "42"), - {1} = encode_decode_sophia_string("bool", "true"), - {0} = encode_decode_sophia_string("bool", "false"), - {<<"Hello">>} = encode_decode_sophia_string("string", "\"Hello\""), - {<<"Hello">>, [1,2,3], {variant, 1, [1]}} = - encode_decode_sophia_string( - "(string, list(int), option(bool))", - "\"Hello\", [1,2,3], Some(true)"), + Check = fun(Type, Str) -> case {encode_decode_sophia_string(Type, Str), Str} of + {X, X} -> ok; + Other -> Other + end end, + ok = Check("int", "42"), + ok = Check("bool", "true"), + ok = Check("bool", "false"), + ok = Check("string", "\"Hello\""), + ok = Check("(string, list(int), option(bool))", + "(\"Hello\", [1, 2, 3], Some(true))"), + ok = Check("variant", "Blue({[\"x\"] = 1})"), + ok = Check("r", "{x = (\"foo\", 0), y = Red}"), ok. encode_decode_sophia_string(SophiaType, String) -> io:format("String ~p~n", [String]), Code = [ "contract MakeCall =\n" - , " function foo : ", SophiaType, " => _\n" - , " function __call() = foo(", String, ")\n" ], - {ok, _, {Types, _}, Args} = aeso_compiler:check_call(lists:flatten(Code), []), - Arg = list_to_tuple(Args), - Type = {tuple, Types}, - io:format("Type ~p~n", [Type]), - Data = encode(Arg), - decode(Type, Data). + , " type arg_type = ", SophiaType, "\n" + , " type an_alias('a) = (string, 'a)\n" + , " record r = {x : an_alias(int), y : variant}\n" + , " datatype variant = Red | Blue(map(string, int))\n" + , " function foo : arg_type => arg_type\n" ], + case aeso_compiler:check_call(lists:flatten(Code), "foo", [String], []) of + {ok, _, {[Type], _}, [Arg]} -> + io:format("Type ~p~n", [Type]), + Data = encode(Arg), + case aeso_compiler:to_sophia_value(Code, "foo", ok, Data) of + {ok, Sophia} -> + lists:flatten(io_lib:format("~s", [prettypr:format(aeso_pretty:expr(Sophia))])); + {error, Err} -> + io:format("~s\n", [Err]), + {error, Err} + end; + {error, Err} -> + io:format("~s\n", [Err]), + {error, Err} + end. + +calldata_test() -> + [42, <<"foobar">>] = encode_decode_calldata("foo", ["int", "string"], ["42", "\"foobar\""]), + Map = #{ <<"a">> => 4 }, + [{variant, 1, [Map]}, {{<<"b">>, 5}, {variant, 0, []}}] = + encode_decode_calldata("foo", ["variant", "r"], ["Blue({[\"a\"] = 4})", "{x = (\"b\", 5), y = Red}"]), + [16#123, 16#456] = encode_decode_calldata("foo", ["hash", "address"], ["#123", "#456"]), + ok. + +calldata_init_test() -> + encode_decode_calldata("init", ["int"], ["42"], {tuple, [typerep, word]}), + + Code = parameterized_contract("foo", ["int"]), + encode_decode_calldata_(Code, "init", [], {tuple, [typerep, {tuple, []}]}). + +calldata_indent_test() -> + Test = fun(Extra) -> + encode_decode_calldata_( + parameterized_contract(Extra, "foo", ["int"]), + "foo", ["42"], word) + end, + Test(" stateful function bla() = ()"), + Test(" type x = int"), + Test(" private function bla : int => int"), + Test(" public stateful function bla(x : int) =\n" + " x + 1"), + Test(" stateful private function bla(x : int) : int =\n" + " x + 1"), + ok. + +parameterized_contract(FunName, Types) -> + parameterized_contract([], FunName, Types). + +parameterized_contract(ExtraCode, FunName, Types) -> + lists:flatten( + ["contract Dummy =\n", + ExtraCode, "\n", + " type an_alias('a) = (string, 'a)\n" + " record r = {x : an_alias(int), y : variant}\n" + " datatype variant = Red | Blue(map(string, int))\n" + " function ", FunName, " : (", string:join(Types, ", "), ") => int\n" ]). + +oracle_test() -> + Contract = + "contract OracleTest =\n" + " function question(o, q : oracle_query(list(string), option(int))) =\n" + " Oracle.get_question(o, q)\n", + {ok, _, {[word, word], {list, string}}, [16#123, 16#456]} = + aeso_compiler:check_call(Contract, "question", ["#123", "#456"], []), + ok. + +permissive_literals_fail_test() -> + Contract = + "contract OracleTest =\n" + " function haxx(o : oracle(list(string), option(int))) =\n" + " Chain.spend(o, 1000000)\n", + {error, <<"Type errors\nCannot unify", _/binary>>} = + aeso_compiler:check_call(Contract, "haxx", ["#123"], []), + ok. + +encode_decode_calldata(FunName, Types, Args) -> + encode_decode_calldata(FunName, Types, Args, word). + +encode_decode_calldata(FunName, Types, Args, RetType) -> + Code = parameterized_contract(FunName, Types), + encode_decode_calldata_(Code, FunName, Args, RetType). + +encode_decode_calldata_(Code, FunName, Args, RetVMType) -> + {ok, Calldata, CalldataType, RetVMType1} = aeso_compiler:create_calldata(Code, FunName, Args), + ?assertEqual(RetVMType1, RetVMType), + {ok, {_Hash, ArgTuple}} = aeso_heap:from_binary(CalldataType, Calldata), + tuple_to_list(ArgTuple). encode_decode(T, D) -> ?assertEqual(D, decode(T, encode(D))), diff --git a/test/aeso_aci_tests.erl b/test/aeso_aci_tests.erl new file mode 100644 index 0000000..be9cf0f --- /dev/null +++ b/test/aeso_aci_tests.erl @@ -0,0 +1,45 @@ +-module(aeso_aci_tests). + +-include_lib("eunit/include/eunit.hrl"). + + +do_test() -> + test_contract(1), + test_contract(2). + +test_contract(N) -> + {Contract,DecACI} = test_cases(N), + {ok,Enc} = aeso_aci:encode(Contract), + ?assertEqual(DecACI, jsx:decode(Enc)). + +test_cases(1) -> + Contract = <<"contract C =\n" + " function a(i : int) = i+1\n">>, + DecodedACI = [{<<"contract">>, + [{<<"name">>,<<"C">>}, + {<<"type_defs">>,[]}, + {<<"functions">>, + [[{<<"name">>,<<"a">>}, + {<<"arguments">>, + [[{<<"name">>,<<"i">>},{<<"type">>,<<"int">>}]]}, + {<<"type">>,<<"int">>}, + {<<"stateful">>,false}]]}]}], + {Contract,DecodedACI}; + +test_cases(2) -> + Contract = <<"contract C =\n" + " type allan = int\n" + " function a(i : allan) = i+1\n">>, + DecodedACI = [{<<"contract">>, + [{<<"name">>,<<"C">>}, + {<<"type_defs">>, + [[{<<"name">>,<<"allan">>}, + {<<"vars">>,[]}, + {<<"typedef">>,<<"int">>}]]}, + {<<"functions">>, + [[{<<"name">>,<<"a">>}, + {<<"arguments">>, + [[{<<"name">>,<<"i">>},{<<"type">>,<<"int">>}]]}, + {<<"type">>,<<"int">>}, + {<<"stateful">>,false}]]}]}], + {Contract,DecodedACI}. diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 04fa19f..f96feb7 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -202,8 +202,11 @@ failing_contracts() -> <<"The indexed type string (at line 9, column 25) is not a word type">>, <<"The indexed type alias_string (at line 11, column 25) equals string which is not a word type">>]} , {"bad_events2", - [<<"The event constructor BadEvent1 (at line 9, column 7) has too many string values (max 1)">>, - <<"The event constructor BadEvent2 (at line 10, column 7) has too many indexed values (max 3)">>]} + [<<"The event constructor BadEvent1 (at line 9, column 7) has too many non-indexed values (max 1)">>, + <<"The event constructor BadEvent2 (at line 10, column 7) has too many indexed values (max 3)">>, + <<"The event constructor BadEvent3 (at line 11, column 7) has too many non-indexed values (max 1)">>, + <<"The payload type address (at line 11, column 17) should be string">>, + <<"The payload type int (at line 11, column 26) should be string">>]} , {"type_clash", [<<"Cannot unify int\n" " and string\n" diff --git a/test/aeso_eunit_SUITE.erl b/test/aeso_eunit_SUITE.erl index ed19643..695f354 100644 --- a/test/aeso_eunit_SUITE.erl +++ b/test/aeso_eunit_SUITE.erl @@ -12,9 +12,11 @@ groups() -> , aeso_parser_tests , aeso_compiler_tests , aeso_abi_tests + , aeso_aci_tests ]}]. aeso_scan_tests(_Config) -> ok = eunit:test(aeso_scan_tests). aeso_parser_tests(_Config) -> ok = eunit:test(aeso_parser_tests). aeso_compiler_tests(_Config) -> ok = eunit:test(aeso_compiler_tests). aeso_abi_tests(_Config) -> ok = eunit:test(aeso_abi_tests). +aeso_aci_tests(_Config) -> ok = eunit:test(aeso_aci_tests). diff --git a/test/contracts/bad_events2.aes b/test/contracts/bad_events2.aes index 02842e3..42843f3 100644 --- a/test/contracts/bad_events2.aes +++ b/test/contracts/bad_events2.aes @@ -8,6 +8,7 @@ contract Events = | Event2(alias_string, indexed alias_address) | BadEvent1(string, string) | BadEvent2(indexed int, indexed int, indexed int, indexed address) + | BadEvent3(address, int) function f1(x : int, y : string) = Chain.event(Event1(x, x+1, y))