machinery for running QuickCheck #505

Closed
zxq9 wants to merge 2 commits from quickcheck-ci into master
23 changed files with 531 additions and 229 deletions
Showing only changes of commit be9935cd7e - Show all commits

37
.circleci/config.yml Normal file
View File

@ -0,0 +1,37 @@
version: 2.1
executors:
aebuilder:
docker:
- image: aeternity/builder
user: builder
working_directory: ~/aesophia
jobs:
build:
executor: aebuilder
steps:
- checkout
- restore_cache:
keys:
- dialyzer-cache-v2-{{ .Branch }}-{{ .Revision }}
- dialyzer-cache-v2-{{ .Branch }}-
- dialyzer-cache-v2-
- run:
name: Build
command: rebar3 compile
- run:
name: Static Analysis
command: rebar3 dialyzer
- run:
name: Eunit
command: rebar3 eunit
- run:
name: Common Tests
command: rebar3 ct
- save_cache:
key: dialyzer-cache-v2-{{ .Branch }}-{{ .Revision }}
paths:
- _build/default/rebar3_20.3.8_plt
- store_artifacts:
path: _build/test/logs

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ _build
rebar3.crashdump
current_counterexample.eqc
.qcci
*.erl~
*.aes~

View File

@ -6,5 +6,11 @@ For more information about æternity smart contracts and the sophia language see
It is an OTP application written in Erlang and is by default included in
[the æternity node](https://github.com/aeternity/epoch). However, it can
also be included in other system to compile contracts coded in sophia which
also be included in other systems to compile contracts coded in sophia which
can then be loaded into the æternity system.
## Interface Modules
The basic modules for interfacing the compiler:
* [aeso_compiler: the Sophia compiler](./docs/aeso_compiler.md)

86
docs/aeso_compiler.md Normal file
View File

@ -0,0 +1,86 @@
# aeso_compiler
### Module
### aeso_compiler
The Sophia compiler
### Description
This module provides the interface to the standard Sophia compiler. It
returns the compiled module in a map which can then be loaded.
### Types
``` erlang
contract_string() = string() | binary()
contract_map() = #{bytecode => binary(),
compiler_version => string(),
contract_souce => string(),
type_info => type_info()}
type_info()
errorstring() = binary()
```
### Exports
#### file(File)
#### file(File, Options) -> CompRet
#### from_string(ContractString, Options) -> CompRet
Types
``` erlang
ContractString = contract_string()
Options = [Option]
CompRet = {ok,ContractMap} | {error,ErrorString}
ContractMap = contract_map()
ErrorString = errorstring()
```
Compile a contract defined in a file or in a string.
The **pp_** options all print to standard output the following:
`pp_sophia_code` - print the input Sophia code.
`pp_ast` - print the AST of the code
`pp_types` - print information about the types
`pp_typed_ast` - print the AST with type information at each node
`pp_icode` - print the internal code structure
`pp_assembler` - print the generated assembler code
`pp_bytecode` - print the bytecode instructions
#### check_call(ContractString, Options) -> CheckRet
Types
```
ContractString = string() | binary()
CheckRet = {ok,string(),{Types,Type | any()},Terms} | {error,Term}
Types = [Type]
Type = term()
```
Check a call in contract through the `__call` function.
#### sophia_type_to_typerep(String) -> TypeRep
Types
``` erlang
{ok,TypeRep} | {error, badtype}
```
Get the type representation of a type declaration.
#### version() -> Version
Types
``` erlang
Version = integer()
```
Get the current version of the Sophia compiler.

View File

@ -1,16 +1,32 @@
{erl_opts, [debug_info]}.
%% NOTE: When possible deps are referenced by Git ref to ensure consistency between builds.
{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git",
{ref,"720510a"}}}
, {getopt, "1.0.1"}
]}.
{profiles, [ {eqc, [{erl_opts, [{parse_transform, eqc_cover}]},
{deps, [{eqc_ci, "1.0.0"}]},
{extra_src_dirs, ["quickcheck"]} %% May not be called eqc!
]}
]}.
{escript_incl_apps, [aesophia, aebytecode, getopt]}.
{escript_main_app, aesophia}.
{escript_name, aesophia}.
{escript_emu_args, "%%! +sbtu +A0\n"}.
{provider_hooks, [{post, [{compile, escriptize}]}]}.
{post_hooks, [{"(linux|darwin|solaris|freebsd|netbsd|openbsd)",
escriptize,
"cp \"$REBAR_BUILD_DIR/bin/aesophia\" ./aesophia"},
{"win32",
escriptize,
"robocopy \"%REBAR_BUILD_DIR%/bin/\" ./ aesophia* "
"/njs /njh /nfl /ndl & exit /b 0"} % silence things
]}.
{dialyzer, [
{warnings, [unknown]},
{plt_apps, all_deps},

View File

@ -1,4 +1,10 @@
{"1.1.0",
[{<<"aebytecode">>,
{git,"https://github.com/aeternity/aebytecode.git",
{ref,"720510a24de32c9bad6486f34ca7babde124bf1e"}},
0}].
0},
{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},0}]}.
[
{pkg_hash,[
{<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]}
].

View File

@ -27,8 +27,8 @@
-type typerep() :: aeso_sophia:type().
-type function_type_info() :: { FunctionHash :: hash()
, FunctionName :: function_name()
, ArgType :: aeso_sophia:heap() %% binary typerep
, OutType :: aeso_sophia:heap() %% binary typerep
, ArgType :: binary() %% binary typerep
, OutType :: binary() %% binary typerep
}.
-type type_info() :: [function_type_info()].
@ -84,8 +84,8 @@ check_given_type(FunName, GivenArgs, GivenRet, CalldataType, ExpectRet) ->
{expected, ExpectArgs, '=>', ExpectRet}}}
end.
-spec check_calldata(aeso_sophia:heap(), type_info()) ->
{'ok', typerep()} | {'error', atom()}.
-spec check_calldata(binary(), type_info()) ->
{'ok', typerep(), typerep()} | {'error', atom()}.
check_calldata(CallData, TypeInfo) ->
%% The first element of the CallData should be the function name
case get_function_hash_from_calldata(CallData) of
@ -153,7 +153,7 @@ arg_typerep_from_function(Function, TypeInfo) ->
end.
-spec typereps_from_type_hash(hash(), type_info()) ->
{'ok', typerep()} | {'error', 'bad_type_data' | 'unknown_function'}.
{'ok', typerep(), typerep()} | {'error', 'bad_type_data' | 'unknown_function'}.
typereps_from_type_hash(TypeHash, TypeInfo) ->
case lists:keyfind(TypeHash, 1, TypeInfo) of
{TypeHash,_Function, ArgTypeBin, OutTypeBin} ->

View File

@ -17,11 +17,12 @@ line({symbol, Line, _}) -> Line.
symbol_name({symbol, _, Name}) -> Name.
pp(Ast) ->
%% TODO: Actually do *Pretty* printing.
io:format("~p~n", [Ast]).
%% io:format("Tree:\n~p\n",[Ast]),
String = prettypr:format(aeso_pretty:decls(Ast, [])),
io:format("Ast:\n~s\n", [String]).
pp_typed(TypedAst) ->
%% io:format("Typed tree:\n~p\n",[TypedAst]),
String = prettypr:format(aeso_pretty:decls(TypedAst, [show_generated])),
%%io:format("Typed tree:\n~p\n",[TypedAst]),
io:format("Type info:\n~s\n",[String]).
io:format("Type ast:\n~s\n",[String]).

View File

@ -79,6 +79,7 @@ global_env() ->
Event = {id, Ann, "event"},
State = {id, Ann, "state"},
Hash = {id, Ann, "hash"},
Bits = {id, Ann, "bits"},
Oracle = fun(Q, R) -> {app_t, Ann, {id, Ann, "oracle"}, [Q, R]} end,
Query = fun(Q, R) -> {app_t, Ann, {id, Ann, "oracle_query"}, [Q, R]} end,
Unit = {tuple_t, Ann, []},
@ -159,6 +160,16 @@ global_env() ->
{["String", "sha3"], Fun1(String, Hash)},
{["String", "sha256"], Fun1(String, Hash)},
{["String", "blake2b"], Fun1(String, Hash)},
%% Bits
{["Bits", "set"], Fun([Bits, Int], Bits)},
{["Bits", "clear"], Fun([Bits, Int], Bits)},
{["Bits", "test"], Fun([Bits, Int], Bool)},
{["Bits", "sum"], Fun1(Bits, Int)},
{["Bits", "intersection"], Fun([Bits, Bits], Bits)},
{["Bits", "union"], Fun([Bits, Bits], Bits)},
{["Bits", "difference"], Fun([Bits, Bits], Bits)},
{["Bits", "none"], Bits},
{["Bits", "all"], Bits},
%% Conversion
{["Int", "to_str"], Fun1(Int, String)},
{["Address", "to_str"], Fun1(Address, String)}
@ -691,8 +702,7 @@ infer_infix({BoolOp, As})
{fun_t, As, [], [Bool,Bool], Bool};
infer_infix({IntOp, As})
when IntOp == '+'; IntOp == '-'; IntOp == '*'; IntOp == '/';
IntOp == '^'; IntOp == 'mod'; IntOp == 'bsl'; IntOp == 'bsr';
IntOp == 'band'; IntOp == 'bor'; IntOp == 'bxor' ->
IntOp == '^'; IntOp == 'mod' ->
Int = {id, As, "int"},
{fun_t, As, [], [Int, Int], Int};
infer_infix({RelOp, As})
@ -714,8 +724,7 @@ infer_infix({'++', As}) ->
infer_prefix({'!',As}) ->
Bool = {id, As, "bool"},
{fun_t, As, [], [Bool], Bool};
infer_prefix({IntOp,As})
when IntOp =:= '-'; IntOp =:= 'bnot' ->
infer_prefix({IntOp,As}) when IntOp =:= '-' ->
Int = {id, As, "int"},
{fun_t, As, [], [Int], Int}.

View File

@ -348,6 +348,33 @@ ast_body(?qid_app(["String", "concat"], [String1, String2], _, _), Icode) ->
ast_body(?qid_app(["String", "sha3"], [String], _, _), Icode) ->
#unop{ op = 'sha3', rand = ast_body(String, Icode) };
%% -- Bits
ast_body(?qid_app(["Bits", Fun], Args, _, _), Icode)
when Fun == "test"; Fun == "set"; Fun == "clear";
Fun == "union"; Fun == "intersection"; Fun == "difference" ->
C = fun(N) when is_integer(N) -> #integer{ value = N };
(X) -> X end,
Bin = fun(O) -> fun(A, B) -> #binop{ op = O, left = C(A), right = C(B) } end end,
And = Bin('band'),
Or = Bin('bor'),
Bsl = fun(A, B) -> (Bin('bsl'))(B, A) end, %% flipped arguments
Bsr = fun(A, B) -> (Bin('bsr'))(B, A) end,
Neg = fun(A) -> #unop{ op = 'bnot', rand = C(A) } end,
case [Fun | [ ast_body(Arg, Icode) || Arg <- Args ]] of
["test", Bits, Ix] -> And(Bsr(Bits, Ix), 1);
["set", Bits, Ix] -> Or(Bits, Bsl(1, Ix));
["clear", Bits, Ix] -> And(Bits, Neg(Bsl(1, Ix)));
["union", A, B] -> Or(A, B);
["intersection", A, B] -> And(A, B);
["difference", A, B] -> And(A, Neg(And(A, B)))
end;
ast_body({qid, _, ["Bits", "none"]}, _Icode) ->
#integer{ value = 0 };
ast_body({qid, _, ["Bits", "all"]}, _Icode) ->
#integer{ value = 1 bsl 256 - 1 };
ast_body(?qid_app(["Bits", "sum"], [Bits], _, _), Icode) ->
builtin_call(popcount, [ast_body(Bits, Icode), #integer{ value = 0 }]);
%% -- Conversion
ast_body(?qid_app(["Int", "to_str"], [Int], _, _), Icode) ->
builtin_call(int_to_str, [ast_body(Int, Icode)]);
@ -526,9 +553,6 @@ ast_binop(Op, Ann, {typed, _, A, Type}, B, Icode)
ast_binop('++', _, A, B, Icode) ->
#funcall{ function = #var_ref{ name = {builtin, list_concat} },
args = [ast_body(A, Icode), ast_body(B, Icode)] };
%% Bit shift operations takes their arguments backwards!?
ast_binop(Op, _, A, B, Icode) when Op =:= 'bsl'; Op =:= 'bsr' ->
#binop{op = Op, right = ast_body(A, Icode), left = ast_body(B, Icode)};
ast_binop(Op, _, A, B, Icode) ->
#binop{op = Op, left = ast_body(A, Icode), right = ast_body(B, Icode)}.

View File

@ -38,7 +38,7 @@ builtin_deps1({map_upd, Type}) -> [{map_get, Type}, map_put];
builtin_deps1({map_upd_default, Type}) -> [{map_lookup_default, Type}, map_put];
builtin_deps1(map_from_list) -> [map_put];
builtin_deps1(str_equal) -> [str_equal_p];
builtin_deps1(string_concat) -> [string_concat_inner1, string_concat_inner2];
builtin_deps1(string_concat) -> [string_concat_inner1, string_copy, string_shift_copy];
builtin_deps1(int_to_str) -> [{baseX_int, 10}];
builtin_deps1(addr_to_str) -> [{baseX_int, 58}];
builtin_deps1({baseX_int, X}) -> [{baseX_int_pad, X}];
@ -144,9 +144,11 @@ builtin_function(BF) ->
string_length -> bfun(BF, builtin_string_length());
string_concat -> bfun(BF, builtin_string_concat());
string_concat_inner1 -> bfun(BF, builtin_string_concat_inner1());
string_concat_inner2 -> bfun(BF, builtin_string_concat_inner2());
string_copy -> bfun(BF, builtin_string_copy());
string_shift_copy -> bfun(BF, builtin_string_shift_copy());
str_equal_p -> bfun(BF, builtin_str_equal_p());
str_equal -> bfun(BF, builtin_str_equal());
popcount -> bfun(BF, builtin_popcount());
int_to_str -> bfun(BF, builtin_int_to_str());
addr_to_str -> bfun(BF, builtin_addr_to_str());
{baseX_int, X} -> bfun(BF, builtin_baseX_int(X));
@ -322,14 +324,17 @@ builtin_string_concat() ->
{[{"s1", string}, {"s2", string}],
?DEREF(n1, s1,
?DEREF(n2, s2,
{ifte, ?EQ(n2, 0),
?V(s1), %% Second string is empty return first string
?LET(ret, {inline_asm, [?A(?MSIZE)]},
{seq, [?ADD(n1, n2), {inline_asm, [?A(?MSIZE), ?A(?MSTORE)]}, %% Store total len
?call(string_concat_inner1, [?V(n1), ?NXT(s1), ?V(n2), ?NXT(s2)]),
{inline_asm, [?A(?POP)]}, %% Discard fun ret val
?V(ret) %% Put the actual return value
]})}
{ifte, ?EQ(n1, 0),
?V(s2), %% First string is empty return second string
{ifte, ?EQ(n2, 0),
?V(s1), %% Second string is empty return first string
?LET(ret, {inline_asm, [?A(?MSIZE)]},
{seq, [?ADD(n1, n2), {inline_asm, [?A(?MSIZE), ?A(?MSTORE)]}, %% Store total len
?call(string_concat_inner1, [?V(n1), ?NXT(s1), ?V(n2), ?NXT(s2)]),
{inline_asm, [?A(?POP)]}, %% Discard fun ret val
?V(ret) %% Put the actual return value
]})}
}
)),
word}.
@ -337,33 +342,33 @@ builtin_string_concat_inner1() ->
%% Copy all whole words from the first string, and set up for word fusion
%% Special case when the length of the first string is divisible by 32.
{[{"n1", word}, {"p1", pointer}, {"n2", word}, {"p2", pointer}],
?DEREF(w1, p1,
{ifte, ?GT(n1, 32),
{seq, [?V(w1), {inline_asm, [?A(?MSIZE), ?A(?MSTORE)]},
?call(string_concat_inner1, [?SUB(n1, 32), ?NXT(p1), ?V(n2), ?V(p2)])]},
{ifte, ?EQ(n1, 0),
?call(string_concat_inner2, [?I(32), ?I(0), ?V(n2), ?V(p2)]),
?call(string_concat_inner2, [?SUB(32, n1), ?V(w1), ?V(n2), ?V(p2)])}
?LET(w1, ?call(string_copy, [?V(n1), ?V(p1)]),
?LET(nx, ?MOD(n1, 32),
{ifte, ?EQ(nx, 0),
?LET(w2, ?call(string_copy, [?V(n2), ?V(p2)]),
{seq, [?V(w2), {inline_asm, [?A(?MSIZE), ?A(?MSTORE), ?A(?MSIZE)]}]}),
?call(string_shift_copy, [?V(nx), ?V(w1), ?V(n2), ?V(p2)])
})),
word}.
builtin_string_copy() ->
{[{"n", word}, {"p", pointer}],
?DEREF(w, p,
{ifte, ?GT(n, 31),
{seq, [?V(w), {inline_asm, [?A(?MSIZE), ?A(?MSTORE)]},
?call(string_copy, [?SUB(n, 32), ?NXT(p)])]},
?V(w)
}),
word}.
builtin_string_concat_inner2() ->
%% Current "work in progess" word 'x', has 'o' bytes that are "free" - fill them from
%% words of the second string.
{[{"o", word}, {"x", word}, {"n2", word}, {"p2", pointer}],
{ifte, ?LT(n2, 1),
{seq, [?V(x), {inline_asm, [?A(?MSIZE), ?A(?MSTORE), ?A(?MSIZE)]}]}, %% Use MSIZE as dummy return value
?DEREF(w2, p2,
{ifte, ?GT(n2, o),
{seq, [?ADD(x, ?BSR(w2, ?SUB(32, o))),
{inline_asm, [?A(?MSIZE), ?A(?MSTORE)]},
?call(string_concat_inner2,
[?V(o), ?BSL(w2, o), ?SUB(n2, 32), ?NXT(p2)])
]},
{seq, [?ADD(x, ?BSR(w2, ?SUB(32, o))),
{inline_asm, [?A(?MSIZE), ?A(?MSTORE), ?A(?MSIZE)]}]} %% Use MSIZE as dummy return value
})
},
builtin_string_shift_copy() ->
{[{"off", word}, {"dst", word}, {"n", word}, {"p", pointer}],
?DEREF(w, p,
{seq, [?ADD(dst, ?BSR(w, off)), {inline_asm, [?A(?MSIZE), ?A(?MSTORE)]},
{ifte, ?GT(n, ?SUB(32, off)),
?call(string_shift_copy, [?V(off), ?BSL(w, ?SUB(32, off)), ?SUB(n, 32), ?NXT(p)]),
{inline_asm, [?A(?MSIZE)]}}]
}),
word}.
builtin_str_equal_p() ->
@ -394,6 +399,17 @@ builtin_str_equal() ->
)),
word}.
%% Count the number of 1s in a bit field.
builtin_popcount() ->
%% function popcount(bits, acc) =
%% if (bits == 0) acc
%% else popcount(bits bsr 1, acc + bits band 1)
{[{"bits", word}, {"acc", word}],
{ifte, ?EQ(bits, 0),
?V(acc),
?call(popcount, [op('bsr', 1, bits), ?ADD(acc, op('band', bits, 1))])
}, word}.
builtin_int_to_str() ->
{[{"i", word}], ?call({baseX_int, 10}, [?V(i)]), word}.

View File

@ -21,8 +21,8 @@
-include("aeso_icode.hrl").
-type option() :: pp_sophia_code | pp_ast | pp_icode | pp_assembler |
pp_bytecode.
-type option() :: pp_sophia_code | pp_ast | pp_types | pp_typed_ast |
pp_icode| pp_assembler | pp_bytecode.
-type options() :: [option()].
-export_type([ option/0
@ -38,34 +38,58 @@
version() ->
?COMPILER_VERSION.
-spec file(string()) -> map().
-spec file(string()) -> {ok, map()} | {error, binary()}.
file(Filename) ->
file(Filename, []).
-spec file(string(), options()) -> map().
file(Filename, Options) ->
C = read_contract(Filename),
from_string(C, Options).
-spec file(string(), options()) -> {ok, map()} | {error, binary()}.
file(File, Options) ->
case read_contract(File) of
{ok, Bin} -> from_string(Bin, Options);
{error, Error} ->
ErrorString = [File,": ",file:format_error(Error)],
{error, join_errors("File errors", [ErrorString], fun(E) -> E end)}
end.
-spec from_string(string(), options()) -> map().
-spec from_string(binary() | string(), options()) -> {ok, map()} | {error, binary()}.
from_string(ContractBin, Options) when is_binary(ContractBin) ->
from_string(binary_to_list(ContractBin), Options);
from_string(ContractString, Options) ->
Ast = parse(ContractString, Options),
ok = pp_sophia_code(Ast, Options),
ok = pp_ast(Ast, Options),
TypedAst = aeso_ast_infer_types:infer(Ast, Options),
%% pp_types is handled inside aeso_ast_infer_types.
ok = pp_typed_ast(TypedAst, Options),
ICode = to_icode(TypedAst, Options),
TypeInfo = extract_type_info(ICode),
ok = pp_icode(ICode, Options),
Assembler = assemble(ICode, Options),
ok = pp_assembler(Assembler, Options),
ByteCodeList = to_bytecode(Assembler, Options),
ByteCode = << << B:8 >> || B <- ByteCodeList >>,
ok = pp_bytecode(ByteCode, Options),
#{byte_code => ByteCode, type_info => TypeInfo,
contract_source => ContractString,
compiler_version => version()}.
try
Ast = parse(ContractString, Options),
ok = pp_sophia_code(Ast, Options),
ok = pp_ast(Ast, Options),
TypedAst = aeso_ast_infer_types:infer(Ast, Options),
%% pp_types is handled inside aeso_ast_infer_types.
ok = pp_typed_ast(TypedAst, Options),
ICode = to_icode(TypedAst, Options),
TypeInfo = extract_type_info(ICode),
ok = pp_icode(ICode, Options),
Assembler = assemble(ICode, Options),
ok = pp_assembler(Assembler, Options),
ByteCodeList = to_bytecode(Assembler, Options),
ByteCode = << << B:8 >> || B <- ByteCodeList >>,
ok = pp_bytecode(ByteCode, Options),
{ok, #{byte_code => ByteCode,
compiler_version => version(),
contract_source => ContractString,
type_info => TypeInfo
}}
catch
%% The compiler errors.
error:{parse_errors, Errors} ->
{error, join_errors("Parse errors", Errors, fun(E) -> E end)};
error:{type_errors, Errors} ->
{error, join_errors("Type errors", Errors, fun(E) -> E end)};
error:{code_errors, Errors} ->
{error, join_errors("Code errors", Errors,
fun (E) -> io_lib:format("~p", [E]) end)}
%% General programming errors in the compiler just signal error.
end.
join_errors(Prefix, Errors, Pfun) ->
Ess = [ Pfun(E) || E <- Errors ],
list_to_binary(string:join([Prefix|Ess], "\n")).
-define(CALL_NAME, "__call").
@ -76,30 +100,40 @@ from_string(ContractString, Options) ->
-spec check_call(string(), options()) -> {ok, string(), {[Type], Type | any}, [term()]} | {error, term()}
when Type :: term().
check_call(ContractString, Options) ->
Ast = parse(ContractString, Options),
ok = pp_sophia_code(Ast, Options),
ok = pp_ast(Ast, Options),
TypedAst = aeso_ast_infer_types:infer(Ast, [permissive_address_literals]),
{ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst),
ok = pp_typed_ast(TypedAst, Options),
Icode = to_icode(TypedAst, Options),
ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ],
RetVMType = case RetType of
{id, _, "_"} -> any;
_ -> aeso_ast_to_icode:ast_typerep(RetType, Icode)
end,
ok = pp_icode(Icode, Options),
#{ functions := Funs } = Icode,
ArgIcode = get_arg_icode(Funs),
try [ icode_to_term(T, Arg) || {T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ] of
ArgTerms ->
{ok, FunName, {ArgVMTypes, RetVMType}, ArgTerms}
catch throw:Err ->
{error, Err}
try
Ast = parse(ContractString, Options),
ok = pp_sophia_code(Ast, Options),
ok = pp_ast(Ast, Options),
TypedAst = aeso_ast_infer_types:infer(Ast, [permissive_address_literals]),
{ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst),
ok = pp_typed_ast(TypedAst, Options),
Icode = to_icode(TypedAst, Options),
ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ],
RetVMType = case RetType of
{id, _, "_"} -> any;
_ -> aeso_ast_to_icode:ast_typerep(RetType, Icode)
end,
ok = pp_icode(Icode, Options),
#{ functions := Funs } = Icode,
ArgIcode = get_arg_icode(Funs),
ArgTerms = [ icode_to_term(T, Arg) ||
{T, Arg} <- lists:zip(ArgVMTypes, ArgIcode) ],
{ok, FunName, {ArgVMTypes, RetVMType}, ArgTerms}
catch
error:{parse_errors, Errors} ->
{error, join_errors("Parse errors", Errors, fun (E) -> E end)};
error:{type_errors, Errors} ->
{error, join_errors("Type errors", Errors, fun (E) -> E end)};
error:{badmatch, {error, missing_call_function}} ->
{error, join_errors("Type errors", ["missing __call function"],
fun (E) -> E end)};
throw:Error -> %Don't ask
{error, join_errors("Code errors", [Error],
fun (E) -> io_lib:format("~p", [E]) end)}
end.
-spec create_calldata(map(), string(), string()) ->
{ok, aeso_sophia:heap(), aeso_sophia:type(), aeso_sophia:type()}
{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
@ -113,7 +147,7 @@ create_calldata(Contract, Function, Argument) when is_map(Contract) ->
%% Function should be "foo : type", and
%% Argument should be "Arg1, Arg2, .., ArgN" (no parens)
case string:lexemes(Function, ": ") of
%% If function is a single word fallback to old calldata generation
%% If function is a single word fallback to old calldata generation
[FunName] -> aeso_abi:old_create_calldata(Contract, FunName, Argument);
[FunName | _] ->
Args = lists:map(fun($\n) -> 32; (X) -> X end, Argument), %% newline to space
@ -247,13 +281,9 @@ parse_string(Text) ->
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]}).
parse_error({Line, Pos}, ErrorString) ->
Error = io_lib:format("line ~p, column ~p: ~s", [Line, Pos, ErrorString]),
error({parse_errors, [Error]}).
read_contract(Name) ->
{ok, Bin} = file:read_file(filename:join(contract_path(), lists:concat([Name, ".aes"]))),
binary_to_list(Bin).
contract_path() ->
"apps/aesophia/test/contracts".
file:read_file(Name).

View File

@ -59,6 +59,7 @@ builtin_types() ->
Word = fun([]) -> word end,
#{ "bool" => Word
, "int" => Word
, "bits" => Word
, "string" => fun([]) -> string end
, "address" => Word
, "hash" => Word

View File

@ -176,11 +176,11 @@ expr200() -> infixr(expr300(), binop('||')).
expr300() -> infixr(expr400(), binop('&&')).
expr400() -> infix(expr500(), binop(['<', '>', '=<', '>=', '==', '!='])).
expr500() -> infixr(expr600(), binop(['::', '++'])).
expr600() -> infixl(expr650(), binop(['+', '-', 'bor', 'bxor', 'bsr', 'bsl'])).
expr600() -> infixl(expr650(), binop(['+', '-'])).
expr650() -> ?RULE(many(token('-')), expr700(), prefixes(_1, _2)).
expr700() -> infixl(expr750(), binop(['*', '/', mod, 'band'])).
expr700() -> infixl(expr750(), binop(['*', '/', mod])).
expr750() -> infixl(expr800(), binop(['^'])).
expr800() -> ?RULE(many(choice(token('!'), token('bnot'))), expr900(), prefixes(_1, _2)).
expr800() -> ?RULE(many(token('!')), expr900(), prefixes(_1, _2)).
expr900() -> ?RULE(exprAtom(), many(elim()), elim(_1, _2)).
exprAtom() ->

View File

@ -170,7 +170,7 @@ expr(E) -> expr_p(0, E).
%% -- Not exported -----------------------------------------------------------
-spec name(aeso_syntax:id() | aeso_syntax:con() | aeso_syntax:tvar()) -> doc().
-spec name(aeso_syntax:id() | aeso_syntax:qid() | aeso_syntax:con() | aeso_syntax:qcon() | aeso_syntax:tvar()) -> doc().
name({id, _, Name}) -> text(Name);
name({con, _, Name}) -> text(Name);
name({qid, _, Names}) -> text(string:join(Names, "."));
@ -359,20 +359,14 @@ bin_prec('++') -> {500, 600, 500};
bin_prec('::') -> {500, 600, 500};
bin_prec('+') -> {600, 600, 650};
bin_prec('-') -> {600, 600, 650};
bin_prec('bor') -> {600, 600, 650};
bin_prec('bxor') -> {600, 600, 650};
bin_prec('bsl') -> {600, 600, 650};
bin_prec('bsr') -> {600, 600, 650};
bin_prec('*') -> {700, 700, 750};
bin_prec('/') -> {700, 700, 750};
bin_prec(mod) -> {700, 700, 750};
bin_prec('band') -> {700, 700, 750};
bin_prec('^') -> {750, 750, 800}.
-spec un_prec(aeso_syntax:un_op()) -> {integer(), integer()}.
un_prec('-') -> {650, 650};
un_prec('!') -> {800, 800};
un_prec('bnot') -> {800, 800}.
un_prec('!') -> {800, 800}.
equals(Ann, A, B) ->
{app, [{format, infix} | Ann], {'=', Ann}, [A, B]}.

View File

@ -37,8 +37,7 @@ lexer() ->
, {"[^/*]+|[/*]", skip()} ],
Keywords = ["contract", "import", "let", "rec", "switch", "type", "record", "datatype", "if", "elif", "else", "function",
"stateful", "true", "false", "and", "mod", "public", "private", "indexed", "internal",
"band", "bor", "bxor", "bsl", "bsr", "bnot"],
"stateful", "true", "false", "and", "mod", "public", "private", "indexed", "internal"],
KW = string:join(Keywords, "|"),
Rules =

View File

@ -75,10 +75,10 @@
-type op() :: bin_op() | un_op().
-type bin_op() :: '+' | '-' | '*' | '/' | mod | '^' | 'band' | 'bor' | 'bsl' | 'bsr' | 'bxor'
-type bin_op() :: '+' | '-' | '*' | '/' | mod | '^'
| '++' | '::' | '<' | '>' | '=<' | '>=' | '==' | '!='
| '||' | '&&' | '..'.
-type un_op() :: '-' | '!' | 'bnot'.
-type un_op() :: '-' | '!'.
-type expr()
:: {lam, ann(), [arg()], expr()}

View File

@ -5,8 +5,8 @@
{applications,
[kernel,
stdlib,
enacl,
syntax_tools,
getopt,
aebytecode
]},
{env,[]},

71
src/aesophia.erl Normal file
View File

@ -0,0 +1,71 @@
-module(aesophia).
-export([main/1]).
-define(OPT_SPEC,
[ {src_file, undefined, undefined, string, "Sophia source code file"}
, {verbose, $v, "verbose", undefined, "Verbose output"}
, {help, $h, "help", undefined, "Show this message"}
, {outfile, $o, "out", string, "Output file (experimental)"} ]).
usage() ->
getopt:usage(?OPT_SPEC, "aesophia").
main(Args) ->
case getopt:parse(?OPT_SPEC, Args) of
{ok, {Opts, []}} ->
case proplists:get_value(help, Opts, false) of
false ->
compile(Opts);
true ->
usage()
end;
{ok, {_, NonOpts}} ->
io:format("Can't understand ~p\n\n", [NonOpts]),
usage();
{error, {Reason, Data}} ->
io:format("Error: ~s ~p\n\n", [Reason, Data]),
usage()
end.
compile(Opts) ->
case proplists:get_value(src_file, Opts, undefined) of
undefined ->
io:format("Error: no input source file\n\n"),
usage();
File ->
compile(File, Opts)
end.
compile(File, Opts) ->
Verbose = proplists:get_value(verbose, Opts, false),
OutFile = proplists:get_value(outfile, Opts, undefined),
try
Res = aeso_compiler:file(File, [pp_ast || Verbose]),
write_outfile(OutFile, Res),
io:format("\nCompiled successfully!\n")
catch
%% The compiler errors.
error:{type_errors, Errors} ->
io:format("\n~s\n", [string:join(["** Type errors\n" | Errors], "\n")]);
error:{parse_errors, Errors} ->
io:format("\n~s\n", [string:join(["** Parse errors\n" | Errors], "\n")]);
error:{code_errors, Errors} ->
ErrorStrings = [ io_lib:format("~p", [E]) || E <- Errors ],
io:format("\n~s\n", [string:join(["** Code errors\n" | ErrorStrings], "\n")]);
%% General programming errors in the compiler.
error:Error ->
Where = hd(erlang:get_stacktrace()),
ErrorString = io_lib:format("Error: ~p in\n ~p", [Error,Where]),
io:format("\n~s\n", [ErrorString])
end.
write_outfile(undefined, _) -> ok;
write_outfile(Out, ResMap) ->
%% Lazy approach
file:write_file(Out, term_to_binary(ResMap)),
io:format("Output written to: ~s\n", [Out]).

View File

@ -28,13 +28,15 @@ simple_compile_test_() ->
end} || ContractName <- compilable_contracts() ] ++
[ {"Testing error messages of " ++ ContractName,
fun() ->
{type_errors, Errors} = compile(ContractName),
check_errors(lists:sort(ExpectedErrors), lists:sort(Errors))
<<"Type errors\n",ErrorString/binary>> = compile(ContractName),
check_errors(lists:sort(ExpectedErrors), ErrorString)
end} ||
{ContractName, ExpectedErrors} <- failing_contracts() ]
}.
check_errors(Expect, Actual) ->
check_errors(Expect, ErrorString) ->
%% This removes the final single \n as well.
Actual = binary:split(<<ErrorString/binary,$\n>>, <<"\n\n">>, [global,trim]),
case {Expect -- Actual, Actual -- Expect} of
{[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra});
{Missing, []} -> ?assertMatch({missing, []}, {missing, Missing});
@ -42,10 +44,10 @@ check_errors(Expect, Actual) ->
end.
compile(Name) ->
try
aeso_compiler:from_string(aeso_test_utils:read_contract(Name), [])
catch _:{type_errors, _} = E ->
E
String = aeso_test_utils:read_contract(Name),
case aeso_compiler:from_string(String, []) of
{ok,Map} -> Map;
{error,ErrorString} -> ErrorString
end.
%% compilable_contracts() -> [ContractName].
@ -75,82 +77,94 @@ compilable_contracts() ->
failing_contracts() ->
[ {"name_clash",
["Duplicate definitions of abort at\n - (builtin location)\n - line 14, column 3\n",
"Duplicate definitions of double_def at\n - line 10, column 3\n - line 11, column 3\n",
"Duplicate definitions of double_proto at\n - line 4, column 3\n - line 5, column 3\n",
"Duplicate definitions of proto_and_def at\n - line 7, column 3\n - line 8, column 3\n",
"Duplicate definitions of put at\n - (builtin location)\n - line 15, column 3\n",
"Duplicate definitions of state at\n - (builtin location)\n - line 16, column 3\n"]}
[<<"Duplicate definitions of abort at\n"
" - (builtin location)\n"
" - line 14, column 3">>,
<<"Duplicate definitions of double_def at\n"
" - line 10, column 3\n"
" - line 11, column 3">>,
<<"Duplicate definitions of double_proto at\n"
" - line 4, column 3\n"
" - line 5, column 3">>,
<<"Duplicate definitions of proto_and_def at\n"
" - line 7, column 3\n"
" - line 8, column 3">>,
<<"Duplicate definitions of put at\n"
" - (builtin location)\n"
" - line 15, column 3">>,
<<"Duplicate definitions of state at\n"
" - (builtin location)\n"
" - line 16, column 3">>]}
, {"type_errors",
["Unbound variable zz at line 17, column 21\n",
"Cannot unify int\n"
" and list(int)\n"
"when checking the application at line 26, column 9 of\n"
" (::) : (int, list(int)) => list(int)\n"
"to arguments\n"
" x : int\n"
" x : int\n",
"Cannot unify string\n"
" and int\n"
"when checking the assignment of the field\n"
" x : map(string, string) (at line 9, column 46)\n"
"to the old value __x and the new value\n"
" __x {[\"foo\"] @ x = x + 1} : map(string, int)\n",
"Cannot unify int\n"
" and string\n"
"when checking the type of the expression at line 34, column 45\n"
" 1 : int\n"
"against the expected type\n"
" string\n",
"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 34, column 50\n"
" \"bla\" : string\n"
"against the expected type\n"
" int\n",
"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 32, column 18\n"
" \"x\" : string\n"
"against the expected type\n"
" int\n",
"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 11, column 56\n"
" \"foo\" : string\n"
"against the expected type\n"
" int\n",
"Cannot unify int\n"
" and string\n"
"when comparing the types of the if-branches\n"
" - w : int (at line 38, column 13)\n"
" - z : string (at line 39, column 10)\n",
"Not a record type: string\n"
"arising from the projection of the field y (at line 22, column 38)\n",
"Not a record type: string\n"
"arising from an assignment of the field y (at line 21, column 42)\n",
"Not a record type: string\n"
"arising from an assignment of the field y (at line 20, column 38)\n",
"Not a record type: string\n"
"arising from an assignment of the field y (at line 19, column 35)\n",
"Ambiguous record type with field y (at line 13, column 25) could be one of\n"
" - r (at line 4, column 10)\n"
" - r' (at line 5, column 10)\n",
"Record type r2 does not have field y (at line 15, column 22)\n",
"The field z is missing when constructing an element of type r2 (at line 15, column 24)\n",
"Repeated name x in pattern\n"
" x :: x (at line 26, column 7)\n",
"No record type with fields y, z (at line 14, column 22)\n"]}
[<<"Unbound variable zz at line 17, column 21">>,
<<"Cannot unify int\n"
" and list(int)\n"
"when checking the application at line 26, column 9 of\n"
" (::) : (int, list(int)) => list(int)\n"
"to arguments\n"
" x : int\n"
" x : int">>,
<<"Cannot unify string\n"
" and int\n"
"when checking the assignment of the field\n"
" x : map(string, string) (at line 9, column 46)\n"
"to the old value __x and the new value\n"
" __x {[\"foo\"] @ x = x + 1} : map(string, int)">>,
<<"Cannot unify int\n"
" and string\n"
"when checking the type of the expression at line 34, column 45\n"
" 1 : int\n"
"against the expected type\n"
" string">>,
<<"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 34, column 50\n"
" \"bla\" : string\n"
"against the expected type\n"
" int">>,
<<"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 32, column 18\n"
" \"x\" : string\n"
"against the expected type\n"
" int">>,
<<"Cannot unify string\n"
" and int\n"
"when checking the type of the expression at line 11, column 56\n"
" \"foo\" : string\n"
"against the expected type\n"
" int">>,
<<"Cannot unify int\n"
" and string\n"
"when comparing the types of the if-branches\n"
" - w : int (at line 38, column 13)\n"
" - z : string (at line 39, column 10)">>,
<<"Not a record type: string\n"
"arising from the projection of the field y (at line 22, column 38)">>,
<<"Not a record type: string\n"
"arising from an assignment of the field y (at line 21, column 42)">>,
<<"Not a record type: string\n"
"arising from an assignment of the field y (at line 20, column 38)">>,
<<"Not a record type: string\n"
"arising from an assignment of the field y (at line 19, column 35)">>,
<<"Ambiguous record type with field y (at line 13, column 25) could be one of\n"
" - r (at line 4, column 10)\n"
" - r' (at line 5, column 10)">>,
<<"Record type r2 does not have field y (at line 15, column 22)">>,
<<"The field z is missing when constructing an element of type r2 (at line 15, column 24)">>,
<<"Repeated name x in pattern\n"
" x :: x (at line 26, column 7)">>,
<<"No record type with fields y, z (at line 14, column 22)">>]}
, {"init_type_error",
["Cannot unify string\n"
" and map(int, int)\n"
"when checking that 'init' returns a value of type 'state' at line 7, column 3\n"]}
[<<"Cannot unify string\n"
" and map(int, int)\n"
"when checking that 'init' returns a value of type 'state' at line 7, column 3">>]}
, {"missing_state_type",
["Cannot unify string\n"
" and ()\n"
"when checking that 'init' returns a value of type 'state' at line 5, column 3\n"]}
[<<"Cannot unify string\n"
" and ()\n"
"when checking that 'init' returns a value of type 'state' at line 5, column 3">>]}
, {"missing_fields_in_record_expression",
["The field x is missing when constructing an element of type r('a) (at line 7, column 40)\n",
"The field y is missing when constructing an element of type r(int) (at line 8, column 40)\n",
"The fields y, z are missing when constructing an element of type r('1) (at line 6, column 40)\n"]}
[<<"The field x is missing when constructing an element of type r('a) (at line 7, column 40)">>,
<<"The field y is missing when constructing an element of type r(int) (at line 8, column 40)">>,
<<"The fields y, z are missing when constructing an element of type r('1) (at line 6, column 40)">>]}
].

View File

@ -36,8 +36,6 @@ contract AllSyntax =
(x, [y, z]) => bar({x = z, y = -y + - -z * (-1)})
(x, y :: _) => ()
function bitOperations(x, y) = bnot (0xff00 band x bsl 4 bxor 0xa5a5a5 bsr 4 bor y)
function mutual() =
let rec recFun(x : int) = mutFun(x)
and mutFun(x) = if(x =< 0) 1 else x * recFun(x - 1)

View File

@ -5,7 +5,7 @@
contract MultiSig =
record pending_state = { yetNeeded : uint, ownersDone : uint, index : uint }
record pending_state = { yetNeeded : int, ownersDone : bits, index : int }
datatype event =
Confirmation (address, hash) // of { .owner : Address, .operation : Hash }
@ -13,18 +13,18 @@ contract MultiSig =
| OwnerChanged (address, address) // of { .oldOwner : Address, .newOwner : Address }
| OwnerAdded (address) // of { .newOwner : Address }
| OwnerRemoved (address) // of { .removedOwner : Address }
| ReqChanged (uint) // of { .newReq : uint }
| ReqChanged (int) // of { .newReq : int }
let maxOwners : uint = 250
let maxOwners : int = 250
record state = { nRequired : uint
, nOwners : uint
, owners : map(uint, address)
, ownerIndex : map(address, uint)
record state = { nRequired : int
, nOwners : int
, owners : map(int, address)
, ownerIndex : map(address, int)
, pending : map(hash, pending_state)
, pendingIndex : list(address) }
function init (owners : list(address), nRequired : uint) : state =
function init (owners : list(address), nRequired : int) : state =
let n = length(owners) + 1
{ nRequired = nRequired,
nOwners = n,
@ -39,10 +39,9 @@ contract MultiSig =
function revoke(operation : hash) =
let ownerIx = lookup(state.ownerIndex, caller())
let pending = lookup(state.pendingIndex, operation)
let ownerIxBit = 1 bsl (ownerIx - 1)
let _ = require(pending.ownersDone band ownerIxBit > 0)
let _ = require(Bits.test(pending.ownersDone, ownerIx))
let pending' = pending { yetNeeded = pending.yetNeeded + 1
, ownersDone = pending.ownersDone - ownerIxBit }
, ownersDone = Bits.clear(pending.ownersDone, ownerIx - 1) }
put(state{ pendingIndex.operator = pending' })
event(Revoke(caller, operation))
@ -91,7 +90,7 @@ contract MultiSig =
, pendingIx = [] },
event = [OwnerRemoved(oldOwner)] }
function changeRequirement(newReq : uint) =
function changeRequirement(newReq : int) =
let _ = require(newReq =< state.nOwners)
switch(check_pending(callhash()))
CheckFail(state') => { state = state' }
@ -102,7 +101,7 @@ contract MultiSig =
event = [ReqChanged(newReq)] }
function getOwner(ownerIx0 : uint) =
function getOwner(ownerIx0 : int) =
lookup(state.owners, ownerIx0 + 1)
function isOwner(owner : address) =
@ -116,8 +115,7 @@ contract MultiSig =
Some(pending) =>
let _ = require(isOwner(owner))
let ownerIx = lookup(state.ownerIndex, owner)
let ownerIxBit = 1 bsl (ownerIx - 1)
(pending.ownersDone band ownerIxBit) != 0
Bits.test(pending.ownersDone, ownerIx - 1)
/* Leave the rest for now... */

View File

@ -1,5 +1,4 @@
// - + * / mod arithmetic operators
// bnot band bor bxor bsl bsr bitwise operators
// ! && || logical operators
// == != < > =< >= comparison operators
// :: ++ list operators
@ -13,12 +12,6 @@ contract Operators =
"/" => a / b
"mod" => a mod b
"^" => a ^ b
"bnot" => bnot a
"band" => a band b
"bor" => a bor b
"bxor" => a bxor b
"bsl" => a bsl b
"bsr" => a bsr b
function bool_op(a : bool, b : bool, op : string) =
switch(op)