diff --git a/README.md b/README.md index f4f9555..a7c982c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # aesophia -This is the __sophia__ compiler for the æternity system which compiles contracts written in __sophia__ code to the æternity VM code. - -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 systems to compile contracts coded in sophia which -can then be loaded into the æternity system. +This is the __sophia__ compiler for the æternity system which compiles contracts written in __sophia__ to [FATE](https://github.com/aeternity/protocol/blob/master/contracts/fate.md) instructions. +The compiler is currently being used three places + - [The command line compiler](https://github.com/aeternity/aesophia_cli) + - [The HTTP compiler](https://github.com/aeternity/aesophia_http) + - In [Aeternity node](https://github.com/aeternity/aeternity) tests ## Documentation @@ -14,7 +13,6 @@ can then be loaded into the æternity system. * [Sophia Documentation](docs/sophia.md). * [Sophia Standard Library](docs/sophia_stdlib.md). - ## Versioning `aesophia` has a version that is only loosely connected to the version of the @@ -23,7 +21,6 @@ minor/patch version. The `aesophia` compiler version MUST be bumped whenever there is a change in how byte code is generated, but it MAY also be bumped upon API changes etc. - ## Interface Modules The basic modules for interfacing the compiler: diff --git a/docs/sophia.md b/docs/sophia.md index 6478a1d..ffcf2f8 100644 --- a/docs/sophia.md +++ b/docs/sophia.md @@ -83,7 +83,7 @@ The main unit of code in Sophia is the *contract*. - A contract may define a type `state` encapsulating its local state. When creating a new contract the `init` entrypoint is executed and the state is initialized to its return value. - + The language offers some primitive functions to interact with the blockchain and contracts. Please refer to the [Chain](sophia_stdlib.md#Chain), [Contract](sophia_stdlib.md#Contract) and the [Call](sophia_stdlib.md#Call) namespaces in the documentation. @@ -314,7 +314,7 @@ so even cyclic includes should be working without any special tinkering. ### Standard library -Sophia offers [standard library](sophia_stdlib.md) which exposes some +Sophia offers [standard library](sophia_stdlib.md) which exposes some primitive operations and some higher level utilities. The builtin namespaces like `Chain`, `Contract`, `Map` are included by default and are supported internally by the compiler. @@ -481,7 +481,7 @@ Example syntax: Lists can be constructed using the range syntax using special `..` operator: ``` -[1..4] == [1,2,3,4] +[1..4] == [1,2,3,4] ``` The ranges are always ascending and have step equal to 1. @@ -595,7 +595,7 @@ Please refer to the `Bytes` [library documentation](sophia_stdlib.md#Bytes). ### Cryptographic builins -Libraries [Crypto](sophia_stdlib.md#Crypto) and [String](sophia_stdlib.md#String) provide functions to +Libraries [Crypto](sophia_stdlib.md#Crypto) and [String](sophia_stdlib.md#String) provide functions to hash objects, verify signatures etc. The `hash` is a type alias for `bytes(32)`. #### AEVM note @@ -630,7 +630,7 @@ Example for an oracle answering questions of type `string` with answers of type contract Oracles = stateful entrypoint registerOracle(acct : address, - sign : signature, // Signed oracle address + contract address + sign : signature, // Signed network id + oracle address + contract address qfee : int, ttl : Chain.ttl) : oracle(string, int) = Oracle.register(acct, signature = sign, qfee, ttl) @@ -651,13 +651,13 @@ contract Oracles = Oracle.extend(o, ttl) stateful entrypoint signExtendOracle(o : oracle(string, int), - sign : signature, // Signed oracle address + contract address - ttl : Chain.ttl) : unit = + sign : signature, // Signed network id + oracle address + contract address + ttl : Chain.ttl) : unit = Oracle.extend(o, signature = sign, ttl) stateful entrypoint respond(o : oracle(string, int), q : oracle_query(string, int), - sign : signature, // Signed oracle query id + contract address + sign : signature, // Signed network id + oracle query id + contract address r : int) = Oracle.respond(o, q, signature = sign, r) @@ -678,7 +678,7 @@ contract Oracles = #### Sanity checks -When an Oracle literal is passed to a contract, no deep checks are performed. +When an Oracle literal is passed to a contract, no deep checks are performed. For extra safety [Oracle.check](sophia_stdlib.md#check) and [Oracle.check_query](sophia_stdlib.md#check_query) functions are provided. @@ -738,7 +738,7 @@ To use events a contract must declare a datatype `event`, and events are then logged using the `Chain.event` function: ``` - datatype event + datatype event = Event1(int, int, string) | Event2(string, address) diff --git a/docs/sophia_stdlib.md b/docs/sophia_stdlib.md index 7b82949..9c58044 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -347,7 +347,7 @@ Registers new oracle answering questions of type `'a` with answers of type `'b`. * The `acct` is the address of the oracle to register (can be the same as the contract). * `signature` is a signature proving that the contract is allowed to register the account - - the account address + the contract address (concatenated as byte arrays) is + the `network id` + `account address` + `contract address` (concatenated as byte arrays) is [signed](./sophia.md#delegation-signature) with the private key of the account, proving you have the private key of the oracle to be. If the address is the same as the contract `sign` is ignored and can be left out entirely. @@ -381,7 +381,8 @@ Responds to the question `q` on `o`. Unless the contract address is the same as the oracle address the `signature` (which is an optional, named argument) needs to be provided. Proving that we have the private key of the oracle by -[signing](./sophia.md#delegation-signature) the oracle query id + contract address +[signing](./sophia.md#delegation-signature) +the `network id` + `oracle query id` + `contract address` #### extend @@ -455,7 +456,8 @@ Naming System (AENS). If `owner` is equal to `Contract.address` the signature `signature` is ignored, and can be left out since it is a named argument. Otherwise we need a signature to prove that we are allowed to do AENS operations on behalf of -`owner` +`owner`. The [signature is tied to a network id](https://github.com/aeternity/protocol/blob/iris/consensus/consensus.md#transaction-signature), +i.e. the signature material should be prefixed by the network id. ### Types @@ -505,8 +507,8 @@ let Some(Name(owner, FixedTTL(expiry), ptrs)) = AENS.lookup("example.chain") AENS.preclaim(owner : address, commitment_hash : hash, ) : unit ``` -The [signature](./sophia.md#delegation-signature) should be over `owner address` + `Contract.address` -(concatenated as byte arrays). +The [signature](./sophia.md#delegation-signature) should be over +`network id` + `owner address` + `Contract.address` (concatenated as byte arrays). #### claim @@ -514,7 +516,9 @@ The [signature](./sophia.md#delegation-signature) should be over `owner address` AENS.claim(owner : address, name : string, salt : int, name_fee : int, ) : unit ``` -The [signature](./sophia.md#delegation-signature) should be over `owner address` + `name_hash` + `Contract.address` +The [signature](./sophia.md#delegation-signature) should be over +`network id` + `owner address` + `name_hash` + `Contract.address` +(concatenated as byte arrays) using the private key of the `owner` account for signing. @@ -525,7 +529,9 @@ AENS.transfer(owner : address, new_owner : address, name : string, ) : unit Revokes the name to extend the ownership time. -The [signature](./sophia.md#delegation-signature) should be over `owner address` + `name_hash` + `Contract.address` +The [signature](./sophia.md#delegation-signature) should be over +`network id` + `owner address` + `name_hash` + `Contract.address` +(concatenated as byte arrays) using the private key of the `owner` account for signing. @@ -773,6 +781,13 @@ List.last(l : list('a)) : option('a) Returns `Some` of the last element of a list or `None` if the list is empty. +#### contains +``` +List.contains(e : 'a, l : list('a)) : bool +``` +Checks if list `l` contains element `e`. Equivalent to `List.find(x => x == e, l) != None`. + + #### find ``` List.find(p : 'a => bool, l : list('a)) : option('a) @@ -1113,6 +1128,13 @@ Option.force(o : option('a)) : 'a Forcefully escapes `option` wrapping assuming it is `Some`. Throws error on `None`. +#### contains +``` +Option.contains(e : 'a, o : option('a)) : bool +``` +Returns `true` if and only if `o` contains element equal to `e`. Equivalent to `Option.match(false, x => x == e, o)`. + + #### on_elem ``` Option.on_elem(o : option('a), f : 'a => unit) : unit diff --git a/priv/stdlib/List.aes b/priv/stdlib/List.aes index c66a591..5354014 100644 --- a/priv/stdlib/List.aes +++ b/priv/stdlib/List.aes @@ -15,8 +15,8 @@ namespace List = _::t => Some(t) function last(l : list('a)) : option('a) = switch(l) - [] => None - [x] => Some(x) + [] => None + [x] => Some(x) _::t => last(t) function drop_last(l : list('a)) : option(list('a)) = switch(l) @@ -28,6 +28,11 @@ namespace List = h::t => h::drop_last_unsafe(t) [] => abort("drop_last_unsafe: list empty") + + function contains(e : 'a, l : list('a)) = switch(l) + [] => false + h::t => h == e || contains(e, t) + /** Finds first element of `l` fulfilling predicate `p` as `Some` or `None` * if no such element exists. */ diff --git a/priv/stdlib/Option.aes b/priv/stdlib/Option.aes index b163e8a..0e00cd4 100644 --- a/priv/stdlib/Option.aes +++ b/priv/stdlib/Option.aes @@ -22,7 +22,11 @@ namespace Option = /** Assume it is `Some` */ - function force(o : option('a)) : 'a = default(abort("Forced None value"), o) + function force(o : option('a)) : 'a = switch(o) + None => abort("Forced None value") + Some(x) => x + + function contains(e : 'a, o : option('a)) = o == Some(e) function on_elem(o : option('a), f : 'a => unit) : unit = match((), f, o) diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 26372a8..233cbec 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -14,6 +14,8 @@ , contract_interface/2 , contract_interface/3 + , from_typed_ast/2 + , render_aci_json/1 , json_encode_expr/1 @@ -23,6 +25,8 @@ -type json() :: jsx:json_term(). -type json_text() :: binary(). +-export_type([aci_type/0]). + %% External API -spec file(aci_type(), string()) -> {ok, json() | string()} | {error, term()}. file(Type, File) -> @@ -65,18 +69,20 @@ do_contract_interface(Type, ContractString, Options) -> try Ast = aeso_compiler:parse(ContractString, Options), %% io:format("~p\n", [Ast]), - TypedAst = aeso_ast_infer_types:infer(Ast, [dont_unfold]), + {TypedAst, _} = aeso_ast_infer_types:infer(Ast, [dont_unfold]), %% io:format("~p\n", [TypedAst]), - JArray = [ encode_contract(C) || C <- TypedAst ], - - case Type of - json -> {ok, JArray}; - string -> do_render_aci_json(JArray) - end + from_typed_ast(Type, TypedAst) catch throw:{error, Errors} -> {error, Errors} end. +from_typed_ast(Type, TypedAst) -> + JArray = [ encode_contract(C) || C <- TypedAst ], + case Type of + json -> {ok, JArray}; + string -> do_render_aci_json(JArray) + end. + encode_contract(Contract = {contract, _, {con, _, Name}, _}) -> C0 = #{name => encode_name(Name)}, diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index b7f54ba..a53b3b1 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -679,7 +679,7 @@ global_env() -> option_t(As, T) -> {app_t, As, {id, As, "option"}, [T]}. map_t(As, K, V) -> {app_t, As, {id, As, "map"}, [K, V]}. --spec infer(aeso_syntax:ast()) -> aeso_syntax:ast() | {env(), aeso_syntax:ast()}. +-spec infer(aeso_syntax:ast()) -> {aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}. infer(Contracts) -> infer(Contracts, []). @@ -689,7 +689,11 @@ infer(Contracts) -> init_env(_Options) -> global_env(). -spec infer(aeso_syntax:ast(), list(option())) -> - aeso_syntax:ast() | {env(), aeso_syntax:ast()}. + {aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}. +infer([], Options) -> + create_type_errors(), + type_error({no_decls, proplists:get_value(src_file, Options, no_file)}), + destroy_and_report_type_errors(init_env(Options)); infer(Contracts, Options) -> ets_init(), %% Init the ETS table state try @@ -698,15 +702,15 @@ infer(Contracts, Options) -> ets_new(type_vars, [set]), check_modifiers(Env, Contracts), {Env1, Decls} = infer1(Env, Contracts, [], Options), - {Env2, Decls2} = + {Env2, DeclsFolded, DeclsUnfolded} = case proplists:get_value(dont_unfold, Options, false) of - true -> {Env1, Decls}; + true -> {Env1, Decls, Decls}; false -> E = on_scopes(Env1, fun(Scope) -> unfold_record_types(Env1, Scope) end), - {E, unfold_record_types(E, Decls)} + {E, Decls, unfold_record_types(E, Decls)} end, case proplists:get_value(return_env, Options, false) of - false -> Decls2; - true -> {Env2, Decls2} + false -> {DeclsFolded, DeclsUnfolded}; + true -> {Env2, DeclsFolded, DeclsUnfolded} end after clean_up_ets() @@ -752,7 +756,9 @@ infer_contract_top(Env, Kind, Defs0, _Options) -> %% a list of definitions. -spec infer_contract(env(), main_contract | contract | namespace, [aeso_syntax:decl()]) -> {env(), [aeso_syntax:decl()]}. infer_contract(Env0, What, Defs0) -> + create_type_errors(), Defs = process_blocks(Defs0), + destroy_and_report_type_errors(Env0), Env = Env0#env{ what = What }, Kind = fun({type_def, _, _, _, _}) -> type; ({letfun, _, _, _, _, _}) -> function; @@ -805,12 +811,12 @@ process_block(Ann, [Decl | Decls]) -> case Decl of {fun_decl, Ann1, Id = {id, _, Name}, Type} -> {Clauses, Rest} = lists:splitwith(IsThis(Name), Decls), - [{fun_clauses, Ann1, Id, Type, Clauses} | - process_block(Ann, Rest)]; + [type_error({mismatched_decl_in_funblock, Name, D1}) || D1 <- Rest], + [{fun_clauses, Ann1, Id, Type, Clauses}]; {letfun, Ann1, Id = {id, _, Name}, _, _, _} -> {Clauses, Rest} = lists:splitwith(IsThis(Name), [Decl | Decls]), - [{fun_clauses, Ann1, Id, {id, [{origin, system} | Ann1], "_"}, Clauses} | - process_block(Ann, Rest)] + [type_error({mismatched_decl_in_funblock, Name, D1}) || D1 <- Rest], + [{fun_clauses, Ann1, Id, {id, [{origin, system} | Ann1], "_"}, Clauses}] end. -spec check_typedefs(env(), [aeso_syntax:decl()]) -> {env(), [aeso_syntax:decl()]}. @@ -2449,6 +2455,12 @@ mk_t_err(Pos, Msg) -> mk_t_err(Pos, Msg, Ctxt) -> aeso_errors:new(type_error, Pos, lists:flatten(Msg), lists:flatten(Ctxt)). +mk_error({no_decls, File}) -> + Pos = aeso_errors:pos(File, 0, 0), + mk_t_err(Pos, "Empty contract\n"); +mk_error({mismatched_decl_in_funblock, Name, Decl}) -> + Msg = io_lib:format("Mismatch in the function block. Expected implementation/type declaration of ~s function\n", [Name]), + mk_t_err(pos(Decl), Msg); mk_error({higher_kinded_typevar, T}) -> Msg = io_lib:format("Type ~s is a higher kinded type variable\n" "(takes another type as an argument)\n", [pp(instantiate(T))] diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index ad5288b..5f3a2cb 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -42,7 +42,8 @@ | {backend, aevm | fate} | {include, {file_system, [string()]} | {explicit_files, #{string() => binary()}}} - | {src_file, string()}. + | {src_file, string()} + | {aci, aeso_aci:aci_type()}. -type options() :: [option()]. -export_type([ option/0 @@ -116,7 +117,8 @@ from_string(Backend, ContractString, Options) -> end. from_string1(aevm, ContractString, Options) -> - #{icode := Icode} = string_to_code(ContractString, Options), + #{ icode := Icode + , folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options), TypeInfo = extract_type_info(Icode), Assembler = assemble(Icode, Options), pp_assembler(aevm, Assembler, Options), @@ -124,47 +126,63 @@ from_string1(aevm, ContractString, Options) -> ByteCode = << << B:8 >> || B <- ByteCodeList >>, pp_bytecode(ByteCode, Options), {ok, Version} = version(), - {ok, #{byte_code => ByteCode, - compiler_version => Version, - contract_source => ContractString, - type_info => TypeInfo, - abi_version => aeb_aevm_abi:abi_version(), - payable => maps:get(payable, Icode) - }}; + Res = #{byte_code => ByteCode, + compiler_version => Version, + contract_source => ContractString, + type_info => TypeInfo, + abi_version => aeb_aevm_abi:abi_version(), + payable => maps:get(payable, Icode) + }, + {ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}; from_string1(fate, ContractString, Options) -> - #{fcode := FCode} = string_to_code(ContractString, Options), + #{ fcode := FCode + , folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options), FateCode = aeso_fcode_to_fate:compile(FCode, Options), pp_assembler(fate, FateCode, Options), ByteCode = aeb_fate_code:serialize(FateCode, []), {ok, Version} = version(), - {ok, #{byte_code => ByteCode, - compiler_version => Version, - contract_source => ContractString, - type_info => [], - fate_code => FateCode, - abi_version => aeb_fate_abi:abi_version(), - payable => maps:get(payable, FCode) - }}. + Res = #{byte_code => ByteCode, + compiler_version => Version, + contract_source => ContractString, + type_info => [], + fate_code => FateCode, + abi_version => aeb_fate_abi:abi_version(), + payable => maps:get(payable, FCode) + }, + {ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}. + +maybe_generate_aci(Result, FoldedTypedAst, Options) -> + case proplists:get_value(aci, Options) of + undefined -> + Result; + Type -> + {ok, Aci} = aeso_aci:from_typed_ast(Type, FoldedTypedAst), + maps:put(aci, Aci, Result) + end. -spec string_to_code(string(), options()) -> map(). string_to_code(ContractString, Options) -> Ast = parse(ContractString, Options), pp_sophia_code(Ast, Options), pp_ast(Ast, Options), - {TypeEnv, TypedAst} = aeso_ast_infer_types:infer(Ast, [return_env | Options]), - pp_typed_ast(TypedAst, Options), + {TypeEnv, FoldedTypedAst, UnfoldedTypedAst} = aeso_ast_infer_types:infer(Ast, [return_env | Options]), + pp_typed_ast(UnfoldedTypedAst, Options), case proplists:get_value(backend, Options, aevm) of aevm -> - Icode = ast_to_icode(TypedAst, Options), + Icode = ast_to_icode(UnfoldedTypedAst, Options), pp_icode(Icode, Options), - #{ icode => Icode, - typed_ast => TypedAst, - type_env => TypeEnv}; + #{ icode => Icode + , unfolded_typed_ast => UnfoldedTypedAst + , folded_typed_ast => FoldedTypedAst + , type_env => TypeEnv + , ast => Ast }; fate -> - Fcode = aeso_ast_to_fcode:ast_to_fcode(TypedAst, Options), - #{ fcode => Fcode, - typed_ast => TypedAst, - type_env => TypeEnv} + Fcode = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, Options), + #{ fcode => Fcode + , unfolded_typed_ast => UnfoldedTypedAst + , folded_typed_ast => FoldedTypedAst + , type_env => TypeEnv + , ast => Ast } end. -define(CALL_NAME, "__call"). @@ -199,9 +217,9 @@ check_call1(ContractString0, FunName, Args, Options) -> case proplists:get_value(backend, Options, aevm) of aevm -> %% First check the contract without the __call function - #{} = string_to_code(ContractString0, Options), - ContractString = insert_call_function(ContractString0, ?CALL_NAME, FunName, Args, Options), - #{typed_ast := TypedAst, + #{ast := Ast} = string_to_code(ContractString0, Options), + ContractString = insert_call_function(Ast, ContractString0, ?CALL_NAME, FunName, Args), + #{unfolded_typed_ast := TypedAst, icode := Icode} = string_to_code(ContractString, Options), {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), ArgVMTypes = [ aeso_ast_to_icode:ast_typerep(T, Icode) || T <- ArgTypes ], @@ -221,13 +239,14 @@ check_call1(ContractString0, FunName, Args, Options) -> {ok, FunName, {ArgVMTypes, RetVMType1}, ArgTerms}; fate -> %% First check the contract without the __call function - #{fcode := OrgFcode} = string_to_code(ContractString0, Options), + #{ fcode := OrgFcode + , ast := Ast } = string_to_code(ContractString0, Options), FateCode = aeso_fcode_to_fate:compile(OrgFcode, []), %% collect all hashes and compute the first name without hash collision to SymbolHashes = maps:keys(aeb_fate_code:symbols(FateCode)), CallName = first_none_match(?CALL_NAME, SymbolHashes, lists:seq($1, $9) ++ lists:seq($A, $Z) ++ lists:seq($a, $z)), - ContractString = insert_call_function(ContractString0, CallName, FunName, Args, Options), + ContractString = insert_call_function(Ast, ContractString0, CallName, FunName, Args), #{fcode := Fcode} = string_to_code(ContractString, Options), CallArgs = arguments_of_body(CallName, FunName, Fcode), {ok, FunName, CallArgs} @@ -253,9 +272,8 @@ first_none_match(CallName, Hashes, [Char|Chars]) -> end. %% Add the __call function to a contract. --spec insert_call_function(string(), string(), string(), [string()], options()) -> string(). -insert_call_function(Code, Call, FunName, Args, Options) -> - Ast = parse(Code, Options), +-spec insert_call_function(aeso_syntax:ast(), string(), string(), string(), [string()]) -> string(). +insert_call_function(Ast, Code, Call, FunName, Args) -> Ind = last_contract_indent(Ast), lists:flatten( [ Code, @@ -311,7 +329,7 @@ to_sophia_value(ContractString, FunName, ok, Data, Options0) -> Options = [no_code | Options0], try Code = string_to_code(ContractString, Options), - #{ typed_ast := TypedAst, type_env := TypeEnv} = Code, + #{ unfolded_typed_ast := TypedAst, type_env := TypeEnv} = Code, {ok, _, Type0} = get_decode_type(FunName, TypedAst), Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, [unfold_record_types, unfold_variant_types]), @@ -387,7 +405,7 @@ decode_calldata(ContractString, FunName, Calldata, Options0) -> Options = [no_code | Options0], try Code = string_to_code(ContractString, Options), - #{ typed_ast := TypedAst, type_env := TypeEnv} = Code, + #{ unfolded_typed_ast := TypedAst, type_env := TypeEnv} = Code, {ok, Args, _} = get_decode_type(FunName, TypedAst), GetType = fun({typed, _, _, T}) -> T; (T) -> T end, diff --git a/src/aeso_syntax.erl b/src/aeso_syntax.erl index 30d4d67..1d01ef0 100644 --- a/src/aeso_syntax.erl +++ b/src/aeso_syntax.erl @@ -39,9 +39,9 @@ | {pragma, ann(), pragma()} | {type_decl, ann(), id(), [tvar()]} % Only for error msgs | {type_def, ann(), id(), [tvar()], typedef()} - | {fun_decl, ann(), id(), type()} - | {fun_clauses, ann(), id(), type(), [letbind()]} + | {fun_clauses, ann(), id(), type(), [letfun() | fundecl()]} | {block, ann(), [decl()]} + | fundecl() | letfun() | letval(). % Only for error msgs @@ -50,8 +50,10 @@ -type pragma() :: {compiler, '==' | '<' | '>' | '=<' | '>=', compiler_version()}. --type letval() :: {letval, ann(), pat(), expr()}. --type letfun() :: {letfun, ann(), id(), [pat()], type(), expr()}. +-type letval() :: {letval, ann(), pat(), expr()}. +-type letfun() :: {letfun, ann(), id(), [pat()], type(), expr()}. +-type fundecl() :: {fun_decl, ann(), id(), type()}. + -type letbind() :: letfun() | letval(). diff --git a/src/aeso_vm_decode.erl b/src/aeso_vm_decode.erl index d25512e..efd7183 100644 --- a/src/aeso_vm_decode.erl +++ b/src/aeso_vm_decode.erl @@ -95,6 +95,8 @@ from_fate({tuple_t, _, Types}, ?FATE_TUPLE(Val)) when length(Types) == tuple_size(Val) -> {tuple, [], [from_fate(Type, X) || {Type, X} <- lists:zip(Types, tuple_to_list(Val))]}; +from_fate({record_t, [{field_t, _, FName, FType}]}, Val) -> + {record, [], [{field, [], [{proj, [], FName}], from_fate(FType, Val)}]}; from_fate({record_t, Fields}, ?FATE_TUPLE(Val)) when length(Fields) == tuple_size(Val) -> {record, [], [ {field, [], [{proj, [], FName}], from_fate(FType, X)} diff --git a/test/aeso_aci_tests.erl b/test/aeso_aci_tests.erl index d5010e3..c851afb 100644 --- a/test/aeso_aci_tests.erl +++ b/test/aeso_aci_tests.erl @@ -11,7 +11,10 @@ test_contract(N) -> {Contract,MapACI,DecACI} = test_cases(N), {ok,JSON} = aeso_aci:contract_interface(json, Contract), ?assertEqual([MapACI], JSON), - ?assertEqual({ok, DecACI}, aeso_aci:render_aci_json(JSON)). + ?assertEqual({ok, DecACI}, aeso_aci:render_aci_json(JSON)), + %% Check if the compiler provides correct aci + {ok,#{aci := JSON2}} = aeso_compiler:from_string(Contract, [{aci, json}]), + ?assertEqual(JSON, JSON2). test_cases(1) -> Contract = <<"payable contract C =\n" @@ -84,7 +87,7 @@ test_cases(3) -> " entrypoint a : (C.bert(string)) => int\n">>, {Contract,MapACI,DecACI}. -%% Rounttrip +%% Roundtrip aci_test_() -> [{"Testing ACI generation for " ++ ContractName, fun() -> aci_test_contract(ContractName) end} @@ -96,6 +99,8 @@ aci_test_contract(Name) -> String = aeso_test_utils:read_contract(Name), Opts = [{include, {file_system, [aeso_test_utils:contract_path()]}}], {ok, JSON} = aeso_aci:contract_interface(json, String, Opts), + {ok, #{aci := JSON1}} = aeso_compiler:from_string(String, [{aci, json}, {backend, fate} | Opts]), + ?assertEqual(JSON, JSON1), io:format("JSON:\n~p\n", [JSON]), {ok, ContractStub} = aeso_aci:render_aci_json(JSON), @@ -122,4 +127,3 @@ check_stub(Stub, Options) -> _ = [ io:format("~s\n", [aeso_errors:pp(E)]) || E <- Errs ], error({parse_errors, Errs}) end. - diff --git a/test/aeso_calldata_tests.erl b/test/aeso_calldata_tests.erl index ff14259..c939893 100644 --- a/test/aeso_calldata_tests.erl +++ b/test/aeso_calldata_tests.erl @@ -113,6 +113,7 @@ compilable_contracts() -> {"funargs", "traffic_light", ["Pantone(12)"]}, {"funargs", "tuples", ["()"]}, %% TODO {"funargs", "due", ["FixedTTL(1020)"]}, + {"funargs", "singleton_rec", ["{x = 1000}"]}, {"variant_types", "init", []}, {"basic_auth", "init", []}, {"address_literals", "init", []}, diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index fae8ea7..9251b8a 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -39,7 +39,7 @@ simple_compile_test_() -> error(ErrBin) end end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], - not lists:member(ContractName, not_yet_compilable(Backend))] ++ + not lists:member(ContractName, not_compilable_on(Backend))] ++ [ {"Test file not found error", fun() -> {error, Errors} = aeso_compiler:file("does_not_exist.aes"), @@ -173,9 +173,13 @@ compilable_contracts() -> "protected_call" ]. -not_yet_compilable(fate) -> []; -not_yet_compilable(aevm) -> ["pairing_crypto", "aens_update", "basic_auth_tx", "more_strings", - "unapplied_builtins", "bytes_to_x", "state_handling", "protected_call"]. +not_compilable_on(fate) -> []; +not_compilable_on(aevm) -> + [ "stdlib_include", "manual_stdlib_include", "pairing_crypto" + , "aens_update", "basic_auth_tx", "more_strings" + , "unapplied_builtins", "bytes_to_x", "state_handling", "protected_call" + ]. + %% Contracts that should produce type errors @@ -681,6 +685,16 @@ failing_contracts() -> " (0 : int) == (1 : int) : bool\n" "It must be either 'true' or 'false'.">> ]) + , ?TYPE_ERROR(bad_function_block, + [<>, + <> + ]) + , ?TYPE_ERROR(just_an_empty_file, + [<> + ]) , ?TYPE_ERROR(bad_number_of_args, [< unit\n" diff --git a/test/contracts/bad_function_block.aes b/test/contracts/bad_function_block.aes new file mode 100644 index 0000000..53f93b1 --- /dev/null +++ b/test/contracts/bad_function_block.aes @@ -0,0 +1,5 @@ +contract C = + function + g(1) = 2 + f(2) = 3 + h(1) = 123 \ No newline at end of file diff --git a/test/contracts/funargs.aes b/test/contracts/funargs.aes index 450c158..f2339e4 100644 --- a/test/contracts/funargs.aes +++ b/test/contracts/funargs.aes @@ -45,3 +45,8 @@ contract FunctionArguments = entrypoint due(t : Chain.ttl) = true + + record singleton_r = { x : int } + + entrypoint singleton_rec(r : singleton_r) = + r.x diff --git a/test/contracts/just_an_empty_file.aes b/test/contracts/just_an_empty_file.aes new file mode 100644 index 0000000..e69de29