From d330133b3fcd6080096d44fa8a9140da2cfda317 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Thu, 24 Jan 2019 18:38:24 +0100 Subject: [PATCH 1/9] First version, very much WIP --- rebar.config | 4 + rebar.lock | 6 +- src/aeso_aci.erl | 216 +++++++++++++++++++++++++++++++++++++++++++ src/aeso_ast.erl | 1 - src/aesophia.app.src | 1 + 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/aeso_aci.erl diff --git a/rebar.config b/rebar.config index 98419de..a98335b 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,"720510a"}}} , {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 37c2aca..13ae7f2 100644 --- a/rebar.lock +++ b/rebar.lock @@ -3,7 +3,11 @@ {git,"https://github.com/aeternity/aebytecode.git", {ref,"720510a24de32c9bad6486f34ca7babde124bf1e"}}, 0}, - {<<"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,[ {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]} diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl new file mode 100644 index 0000000..dff3cc5 --- /dev/null +++ b/src/aeso_aci.erl @@ -0,0 +1,216 @@ +%%%------------------------------------------------------------------- +%%% @author Robert Virding +%%% @copyright (C) 2017, Aeternity Anstalt +%%% @doc +%%% ACI interface +%%% @end +%%% Created : 12 Dec 2017 +%%%------------------------------------------------------------------- + +-module(aeso_aci). + +-export([encode/1,decode/1]). + +%% Define records for the various typed syntactic forms. These don't +%% seem to exist elsewhere. + +-record(contract, {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(arg, {ann,id,type}). +-record(id, {ann,name}). +-record(con, {ann,name}). +-record(tvar, {ann,name}). + +%% encode(ContractString) -> JSON. +%% 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) when is_binary(ContractString) -> + encode(binary_to_list(ContractString)); +encode(ContractString) -> + Options = [], %No options yet + Ast = parse_string(ContractString), + TypedAst = aeso_ast_infer_types:infer(Ast, Options), + %% io:format("~p\n", [Ast]), + %% io:format("~p\n", [TypedAst]), + %% aeso_ast:pp(Ast), + %% aeso_ast:pp_typed(TypedAst), + Cname = contract_name(hd(TypedAst)), + Tdefs = [ encode_typedef(T) || T <- sort_decls(contract_types(hd(TypedAst))) ], + Fdefs = [ encode_func(F) || F <- sort_decls(contract_funcs(hd(TypedAst))) ], + Jmap = [{<<"contract">>, [{<<"name">>, list_to_binary(Cname)}, + {<<"type_defs">>, Tdefs}, + {<<"functions">>, Fdefs}]}], + %% io:format("~p\n", [Jmap]), + jsx:encode(Jmap). + +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))}]. + +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(#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_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, + lists:flatten(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">> := 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). + +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) ]. + +sort_decls(Ds) -> + %% We can easily sort them in annotation, they all should have the + %% same column. + Sort = fun (D1, D2) -> + aeso_syntax:get_ann(D1) =< aeso_syntax:get_ann(D2) + 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. + +%% #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_string(Text) -> + %% Try and return something sensible here! + case aeso_parser:string(Text) 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) + end. + +parse_error({Line,Pos}, ErrorString) -> + Error = io_lib:format("line ~p, column ~p: ~s", [Line,Pos,ErrorString]), + error({parse_errors,[Error]}). 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/aesophia.app.src b/src/aesophia.app.src index 6a47608..d7da977 100644 --- a/src/aesophia.app.src +++ b/src/aesophia.app.src @@ -5,6 +5,7 @@ {applications, [kernel, stdlib, + jsx, syntax_tools, getopt, aebytecode From 2f36380a81439ffc388cbed14050e062e13a6a57 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Fri, 1 Feb 2019 17:00:12 +0100 Subject: [PATCH 2/9] Add handling of private and stateful functions --- rebar.lock | 2 +- src/aeso_aci.erl | 40 ++++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/rebar.lock b/rebar.lock index 13ae7f2..3043376 100644 --- a/rebar.lock +++ b/rebar.lock @@ -7,7 +7,7 @@ {<<"jsx">>, {git,"https://github.com/talentdeficit/jsx.git", {ref,"3074d4865b3385a050badf7828ad31490d860df5"}}, - 0}]. + 0}]}. [ {pkg_hash,[ {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]} diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index dff3cc5..666e8b6 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -45,9 +45,12 @@ encode(ContractString) -> %% io:format("~p\n", [TypedAst]), %% aeso_ast:pp(Ast), %% aeso_ast:pp_typed(TypedAst), - Cname = contract_name(hd(TypedAst)), - Tdefs = [ encode_typedef(T) || T <- sort_decls(contract_types(hd(TypedAst))) ], - Fdefs = [ encode_func(F) || F <- sort_decls(contract_funcs(hd(TypedAst))) ], + %% We look at the first contract. + Contract = hd(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}]}], @@ -60,7 +63,8 @@ encode_func(Fdef) -> Type = function_type(Fdef), [{<<"name">>, list_to_binary(Name)}, {<<"arguments">>, encode_args(Args)}, - {<<"type">>, list_to_binary(encode_type(Type))}]. + {<<"type">>, list_to_binary(encode_type(Type))}, + {<<"stateful">>, is_stateful_func(Fdef)}]. encode_args(Args) -> [ encode_arg(A) || A <- Args ]. @@ -146,18 +150,19 @@ decode_args(As) -> decode_arg(#{<<"type">> := T}) -> decode_type(T). -decode_tdefs(Ts) -> [ decode_tdef(T) || T <- Ts ]. +%% 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_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_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]). +%% decode_tvar(#{<<"name">> := N}) -> io_lib:format("~s", [N]). %% #contract{Ann, Con, [Declarations]}. @@ -170,10 +175,9 @@ contract_types(#contract{decls=Decls}) -> [ D || D <- Decls, is_record(D, type_def) ]. sort_decls(Ds) -> - %% We can easily sort them in annotation, they all should have the - %% same column. Sort = fun (D1, D2) -> - aeso_syntax:get_ann(D1) =< aeso_syntax:get_ann(D2) + aeso_syntax:get_ann(line, D1, 0) =< + aeso_syntax:get_ann(line, D2, 0) end, lists:sort(Sort, Ds). @@ -185,6 +189,10 @@ 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. From cea581988dac8ae7ec38eb15ab9bc6f38960f52a Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Wed, 6 Feb 2019 14:36:16 +0100 Subject: [PATCH 3/9] Don't decode init function as it should never be called --- src/aeso_aci.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 666e8b6..4c8df40 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -136,8 +136,9 @@ decode_contract(#{<<"name">> := Name, %% decode_tdefs(Ts), decode_funcs(Fs)]. -decode_funcs(Fs) -> [ decode_func(F) || F <- 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]. From 5d116b2e5a6aee7df1164c0101e2c8609ae1ca1b Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Wed, 6 Feb 2019 17:07:15 +0100 Subject: [PATCH 4/9] Make the decoder return a binary and untabify --- src/aeso_aci.erl | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 4c8df40..8ec8ce8 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -11,8 +11,8 @@ -export([encode/1,decode/1]). -%% Define records for the various typed syntactic forms. These don't -%% seem to exist elsewhere. +%% 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(letfun, {ann,id,args,type,body}). @@ -38,7 +38,7 @@ encode(ContractString) when is_binary(ContractString) -> encode(binary_to_list(ContractString)); encode(ContractString) -> - Options = [], %No options yet + Options = [], %No options yet Ast = parse_string(ContractString), TypedAst = aeso_ast_infer_types:infer(Ast, Options), %% io:format("~p\n", [Ast]), @@ -50,10 +50,10 @@ encode(ContractString) -> 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) ], + not is_private_func(F) ], Jmap = [{<<"contract">>, [{<<"name">>, list_to_binary(Cname)}, - {<<"type_defs">>, Tdefs}, - {<<"functions">>, Fdefs}]}], + {<<"type_defs">>, Tdefs}, + {<<"functions">>, Fdefs}]}], %% io:format("~p\n", [Jmap]), jsx:encode(Jmap). @@ -126,13 +126,13 @@ decode(Json) -> Map = jsx:decode(Json, [return_maps]), %% io:format("~p\n", [Map]), #{<<"contract">> := C} = Map, - lists:flatten(decode_contract(C)). + list_to_binary(decode_contract(C)). decode_contract(#{<<"name">> := Name, - <<"type_defs">> := _Ts, - <<"functions">> := Fs}) -> + <<"type_defs">> := _Ts, + <<"functions">> := Fs}) -> ["contract"," ",io_lib:format("~s", [Name])," =\n", - [], %Don't include types yet. + [], %Don't include types yet. %% decode_tdefs(Ts), decode_funcs(Fs)]. @@ -153,18 +153,18 @@ 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([]) -> []; %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. @@ -177,9 +177,9 @@ contract_types(#contract{decls=Decls}) -> sort_decls(Ds) -> Sort = fun (D1, D2) -> - aeso_syntax:get_ann(line, D1, 0) =< - aeso_syntax:get_ann(line, D2, 0) - end, + 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}. From 931f2d3dcb29145dfc2b91a13dda27e3d732dcdf Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Thu, 7 Feb 2019 15:45:36 +0100 Subject: [PATCH 5/9] Move module documentation to separate files --- README.md | 1 + docs/aeso_aci.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 docs/aeso_aci.md 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/docs/aeso_aci.md b/docs/aeso_aci.md new file mode 100644 index 0000000..c40956c --- /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) -> JSONstring + +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> 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`. From 6f582af83ec76cc6aad79a5f434160214ac8eac2 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Tue, 26 Feb 2019 02:01:00 +0100 Subject: [PATCH 6/9] Add new encode function interface --- docs/aeso_aci.md | 4 +-- src/aeso_aci.erl | 69 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/docs/aeso_aci.md b/docs/aeso_aci.md index c40956c..082aa6b 100644 --- a/docs/aeso_aci.md +++ b/docs/aeso_aci.md @@ -85,7 +85,7 @@ json_string() = binary() ### Exports -#### encode(ContractString) -> JSONstring +#### encode(ContractString) -> {ok,JSONstring} | {error,ErrorString} Types @@ -114,7 +114,7 @@ This is an example of using the ACI generator from an Erlang shell. The file cal ``` 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> Encoding = aeso_aci:encode(Contract). +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 diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 8ec8ce8..5f9b68a 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -15,6 +15,7 @@ %% 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}). @@ -29,9 +30,12 @@ -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) -> JSON. +%% 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. @@ -39,23 +43,40 @@ encode(ContractString) when is_binary(ContractString) -> encode(binary_to_list(ContractString)); encode(ContractString) -> Options = [], %No options yet - Ast = parse_string(ContractString), - TypedAst = aeso_ast_infer_types:infer(Ast, Options), - %% io:format("~p\n", [Ast]), - %% io:format("~p\n", [TypedAst]), - %% aeso_ast:pp(Ast), - %% aeso_ast:pp_typed(TypedAst), - %% We look at the first contract. - Contract = hd(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]), - jsx:encode(Jmap). + try + Ast = parse_string(ContractString), + TypedAst = aeso_ast_infer_types:infer(Ast, Options), + %% io:format("~p\n", [Ast]), + %% io:format("~p\n", [TypedAst]), + %% aeso_ast:pp(Ast), + %% 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), @@ -79,6 +100,10 @@ encode_types(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),$)]; @@ -175,6 +200,14 @@ contract_funcs(#contract{decls=Decls}) -> contract_types(#contract{decls=Decls}) -> [ D || D <- Decls, is_record(D, type_def) ]. +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) =< From 7448da16bbd16d0906d25b50075f2081cf8e57f2 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Tue, 26 Feb 2019 16:25:23 +0100 Subject: [PATCH 7/9] Make dialyzer happy and keep it quiet --- src/aeso_aci.erl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 5f9b68a..6595415 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -200,13 +200,14 @@ contract_funcs(#contract{decls=Decls}) -> contract_types(#contract{decls=Decls}) -> [ D || D <- Decls, is_record(D, type_def) ]. -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) ]. +%% 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) -> From 0d56130baa6f52b072cf27ef1214f85a340a8d29 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Thu, 28 Feb 2019 16:08:03 +0100 Subject: [PATCH 8/9] Use correct parse error formats --- src/aeso_aci.erl | 77 +++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 6595415..0f7d38c 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -9,7 +9,7 @@ -module(aeso_aci). --export([encode/1,decode/1]). +-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. @@ -26,6 +26,7 @@ -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}). @@ -39,29 +40,30 @@ %% 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) when is_binary(ContractString) -> - encode(binary_to_list(ContractString)); -encode(ContractString) -> - Options = [], %No options yet +encode(ContractString) -> encode(ContractString, []). + +encode(ContractString, Options) when is_binary(ContractString) -> + encode(binary_to_list(ContractString), Options); +encode(ContractString, Options) -> try - Ast = parse_string(ContractString), - TypedAst = aeso_ast_infer_types:infer(Ast, Options), - %% io:format("~p\n", [Ast]), - %% io:format("~p\n", [TypedAst]), - %% aeso_ast:pp(Ast), - %% 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)} + 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} -> @@ -122,7 +124,11 @@ encode_type(#variant_t{cons=Cs}) -> encode_type(#constr_t{con=C,args=As}) -> Ec = encode_type(C), Eas = encode_types(As), - [Ec,$(,lists:join(", ", Eas),$)]. + [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), @@ -236,9 +242,9 @@ typedef_vars(#type_def{vars=Vars}) -> Vars. typedef_def(#type_def{typedef=Def}) -> Def. -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. @@ -251,9 +257,20 @@ 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_error, File}} -> + parse_error(Pos, io_lib:format("could not find include file '~s'", [File])) end. -parse_error({Line,Pos}, ErrorString) -> - Error = io_lib:format("line ~p, column ~p: ~s", [Line,Pos,ErrorString]), - error({parse_errors,[Error]}). +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]). From 8619f47ee6ff2041ca9229dc0b9d2f54d783fb27 Mon Sep 17 00:00:00 2001 From: Robert Virding Date: Thu, 28 Feb 2019 16:09:59 +0100 Subject: [PATCH 9/9] Add very basic ACI testing These should be extended and expanded. --- test/aeso_aci_tests.erl | 45 +++++++++++++++++++++++++++++++++++++++ test/aeso_eunit_SUITE.erl | 2 ++ 2 files changed, 47 insertions(+) create mode 100644 test/aeso_aci_tests.erl 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_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).