Merge pull request #31 from aeternity/new-aci-generator
New aci generator
This commit is contained in:
commit
8f27168908
@ -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)
|
||||
|
135
docs/aeso_aci.md
Normal file
135
docs/aeso_aci.md
Normal file
@ -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`.
|
@ -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]}.
|
||||
|
@ -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">>}]}
|
||||
|
276
src/aeso_aci.erl
Normal file
276
src/aeso_aci.erl
Normal file
@ -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]).
|
@ -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]).
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
{applications,
|
||||
[kernel,
|
||||
stdlib,
|
||||
jsx,
|
||||
syntax_tools,
|
||||
getopt,
|
||||
aebytecode
|
||||
|
45
test/aeso_aci_tests.erl
Normal file
45
test/aeso_aci_tests.erl
Normal file
@ -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}.
|
@ -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).
|
||||
|
Loading…
x
Reference in New Issue
Block a user