From 54e43764ca6e5f4edb61ac387d8ff16a2ebdea1c Mon Sep 17 00:00:00 2001 From: radrow Date: Fri, 15 May 2020 19:09:08 +0200 Subject: [PATCH 01/11] Ban empty contracts, ban function blocks with mismatched declarations --- src/aeso_ast_infer_types.erl | 20 ++++++++++++++++---- src/aeso_syntax.erl | 10 ++++++---- test/aeso_compiler_tests.erl | 10 ++++++++++ test/contracts/bad_function_block.aes | 5 +++++ test/contracts/just_an_empty_file.aes | 0 5 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 test/contracts/bad_function_block.aes create mode 100644 test/contracts/just_an_empty_file.aes diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 84f8683..d85b3f6 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -564,6 +564,10 @@ init_env(_Options) -> global_env(). -spec infer(aeso_syntax:ast(), list(option())) -> aeso_syntax:ast() | {env(), 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 @@ -626,7 +630,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; @@ -679,12 +685,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()]}. @@ -2266,6 +2272,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_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/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index c6f5b9f..e8f5931 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -665,6 +665,16 @@ failing_contracts() -> "Empty record/map update\n" " r {}">> ]) + , ?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/just_an_empty_file.aes b/test/contracts/just_an_empty_file.aes new file mode 100644 index 0000000..e69de29 From 8f240a7ddf197bc561bfa178c471a17c632d2a5f Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Wed, 27 May 2020 08:06:06 +0200 Subject: [PATCH 02/11] Fix AENS types + whitespace --- docs/sophia_stdlib.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sophia_stdlib.md b/docs/sophia_stdlib.md index df13842..da8fd63 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -501,7 +501,7 @@ using the private key of the `owner` account for signing. ### transfer ``` -AENS.transfer(owner : address, new_owner : address, name_hash : hash, ) : unit +AENS.transfer(owner : address, new_owner : address, name : string, ) : unit ``` Transfers name to the new owner. @@ -512,7 +512,7 @@ using the private key of the `owner` account for signing. ### revoke ``` -AENS.revoke(owner : address, name_hash : hash, ) : unit +AENS.revoke(owner : address, name : string, ) : unit ``` Revokes the name to extend the ownership time. @@ -1149,7 +1149,7 @@ will yield `None`. ### choose ``` -Option.choose(o1 : option('a), o2 : option('a)) : option('a) +Option.choose(o1 : option('a), o2 : option('a)) : option('a) ``` Out of two `option`s choose the one that is `Some`, or `None` if both are `None`s. @@ -1261,7 +1261,7 @@ Func.curry2(f : ('a, 'b) => 'c) : 'a => ('b => 'c) Func.curry3(f : ('a, 'b, 'c) => 'd) : 'a => ('b => ('c => 'd)) ``` -Turns a function that takes n arguments into a curried function that takes +Turns a function that takes n arguments into a curried function that takes one argument and returns a function that waits for the rest in the same manner. For instance `curry2((a, b) => a + b)(1)(2) == 3`. @@ -1631,7 +1631,7 @@ This namespace provides operations on rational numbers. A rational number is rep as a fraction of two integers which are stored internally in the `frac` datatype. The datatype consists of three constructors `Neg/2`, `Zero/0` and `Pos/2` which determine the -sign of the number. Both values stored in `Neg` and `Pos` need to be strictly positive +sign of the number. Both values stored in `Neg` and `Pos` need to be strictly positive integers. However, when creating a `frac` you should never use the constructors explicitly. Instead of that, always use provided functions like `make_frac` or `from_int`. This helps keeping the internal representation well defined. @@ -1767,14 +1767,14 @@ Rounds a fraction to the nearest greater or equal integer. ### round_to_zero `Frac.round_to_zero(f : frac) : int` -Rounds a fraction towards zero. +Rounds a fraction towards zero. Effectively `ceil` if lesser than zero and `floor` if greater. ### round_from_zero `Frac.round_from_zero(f : frac) : int` -Rounds a fraction from zero. +Rounds a fraction from zero. Effectively `ceil` if greater than zero and `floor` if lesser. @@ -1806,7 +1806,7 @@ Subtraction of two fractions. ### inv `Frac.inv(a : frac) : frac` -Inverts a fraction. Throws error if `a` is zero. +Inverts a fraction. Throws error if `a` is zero. ### mul From f21717a9c05ece81851054fffacb54981b1b8547 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 29 May 2020 08:35:19 +0200 Subject: [PATCH 03/11] Make README less outdated --- README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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: From cb2588fae2c61454b89229ca90e25d6ba2e6ed8c Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 29 May 2020 08:47:01 +0200 Subject: [PATCH 04/11] Make the network id an explicit part of the signature material --- docs/sophia.md | 20 ++++++++++---------- docs/sophia_stdlib.md | 15 ++++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/sophia.md b/docs/sophia.md index 5179235..228dea9 100644 --- a/docs/sophia.md +++ b/docs/sophia.md @@ -78,7 +78,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. @@ -279,7 +279,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. @@ -446,7 +446,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. @@ -551,7 +551,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 @@ -587,7 +587,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) @@ -608,13 +608,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) @@ -635,7 +635,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. @@ -658,7 +658,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 da8fd63..ab31beb 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -369,7 +369,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 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. @@ -403,7 +403,7 @@ 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 the oracle query id + contract address +signing the `network id` + `oracle query id` + `contract address` ### extend @@ -468,7 +468,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. ### resolve ``` @@ -486,7 +487,7 @@ type checked against this type at run time. AENS.preclaim(owner : address, commitment_hash : hash, ) : unit ``` -The signature should be over `owner address` + `Contract.address` +The signature should be over `network id` + `owner address` + `Contract.address` (concatenated as byte arrays). @@ -495,7 +496,7 @@ The signature should be over `owner address` + `Contract.address` AENS.claim(owner : address, name : string, salt : int, name_fee : int, ) : unit ``` -The signature should be over `owner address` + `name_hash` + `Contract.address` +The signature should be over `network id` + `owner address` + `name_hash` + `Contract.address` using the private key of the `owner` account for signing. @@ -506,7 +507,7 @@ AENS.transfer(owner : address, new_owner : address, name : string, ) : unit Revokes the name to extend the ownership time. -The signature should be over `owner address` + `name_hash` + `Contract.address` +The signature should be over `network id` + `owner address` + `name_hash` + `Contract.address` using the private key of the `owner` account for signing. From eb71abc66570ed55aea30bad5869974742dda90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Rowicki?= <35342116+radrow@users.noreply.github.com> Date: Sat, 6 Jun 2020 14:36:46 +0200 Subject: [PATCH 05/11] Fixed `force` function --- priv/stdlib/Option.aes | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/priv/stdlib/Option.aes b/priv/stdlib/Option.aes index 485ace6..4895e18 100644 --- a/priv/stdlib/Option.aes +++ b/priv/stdlib/Option.aes @@ -22,7 +22,9 @@ 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 on_elem(o : option('a), f : 'a => unit) : unit = match((), f, o) From db4de5d926233cbb3b4bf953a760b93d7bb8307f Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Mon, 10 Aug 2020 16:35:37 +0200 Subject: [PATCH 06/11] Fix singleton record calldata decode + test --- src/aeso_vm_decode.erl | 2 ++ test/aeso_calldata_tests.erl | 1 + test/contracts/funargs.aes | 5 +++++ 3 files changed, 8 insertions(+) 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_calldata_tests.erl b/test/aeso_calldata_tests.erl index 46e0be9..2ae2cbf 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/contracts/funargs.aes b/test/contracts/funargs.aes index b63edff..19c43a2 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 From 7e32ef57c21f572731bc9e99c02288b0a6ff1596 Mon Sep 17 00:00:00 2001 From: radrow Date: Wed, 26 Aug 2020 11:56:18 +0200 Subject: [PATCH 07/11] Added `contains` functions in List and Option. Fixed one type error catch --- docs/sophia_stdlib.md | 14 ++++++++++++++ priv/stdlib/List.aes | 8 ++++++-- priv/stdlib/Option.aes | 4 ++++ test/aeso_compiler_tests.erl | 13 ++++++++----- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/sophia_stdlib.md b/docs/sophia_stdlib.md index ab31beb..6554810 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -700,6 +700,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) @@ -1040,6 +1047,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 3405035..3a56856 100644 --- a/priv/stdlib/List.aes +++ b/priv/stdlib/List.aes @@ -15,10 +15,14 @@ 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 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 4895e18..5e6622f 100644 --- a/priv/stdlib/Option.aes +++ b/priv/stdlib/Option.aes @@ -26,6 +26,10 @@ namespace Option = None => abort("Forced None value") Some(x) => x + function contains(e : 'a, o : option('a)) = switch(o) + None => false + Some(x) => x == e + function on_elem(o : option('a), f : 'a => unit) : unit = match((), f, o) function map(f : 'a => 'b, o : option('a)) : option('b) = switch(o) diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index e8f5931..1254b2c 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"), @@ -168,8 +168,11 @@ compilable_contracts() -> "lhs_matching" ]. -not_yet_compilable(fate) -> []; -not_yet_compilable(aevm) -> []. +not_compilable_on(fate) -> []; +not_compilable_on(aevm) -> + ["stdlib_include", + "manual_stdlib_include" + ]. %% Contracts that should produce type errors @@ -316,8 +319,8 @@ failing_contracts() -> " x : int\n" "against the expected type\n" " string">>, - <>, + <>, <>, < Date: Wed, 26 Aug 2020 12:10:24 +0200 Subject: [PATCH 08/11] minor optimization --- priv/stdlib/Option.aes | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/priv/stdlib/Option.aes b/priv/stdlib/Option.aes index 5e6622f..e364363 100644 --- a/priv/stdlib/Option.aes +++ b/priv/stdlib/Option.aes @@ -26,9 +26,7 @@ namespace Option = None => abort("Forced None value") Some(x) => x - function contains(e : 'a, o : option('a)) = switch(o) - None => false - Some(x) => x == e + function contains(e : 'a, o : option('a)) = o == Some(e) function on_elem(o : option('a), f : 'a => unit) : unit = match((), f, o) From 3d73e52d48bb0b02fa00ae84e2736a9f0f67e6ad Mon Sep 17 00:00:00 2001 From: radrow Date: Wed, 26 Aug 2020 15:56:21 +0200 Subject: [PATCH 09/11] Fix tests --- test/aeso_compiler_tests.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 1254b2c..208839d 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -319,8 +319,8 @@ failing_contracts() -> " x : int\n" "against the expected type\n" " string">>, - <>, + <>, <>, < Date: Wed, 9 Sep 2020 15:33:34 +0200 Subject: [PATCH 10/11] Don't regenerate the AST --- src/aeso_compiler.erl | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index ad5288b..64f3f98 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -157,14 +157,16 @@ string_to_code(ContractString, Options) -> aevm -> Icode = ast_to_icode(TypedAst, Options), pp_icode(Icode, Options), - #{ icode => Icode, - typed_ast => TypedAst, - type_env => TypeEnv}; + #{ icode => Icode + , typed_ast => TypedAst + , 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 => Fcode + , typed_ast => TypedAst + , type_env => TypeEnv + , ast => Ast } end. -define(CALL_NAME, "__call"). @@ -199,8 +201,8 @@ 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), + #{ast := Ast} = string_to_code(ContractString0, Options), + ContractString = insert_call_function(Ast, ContractString0, ?CALL_NAME, FunName, Args), #{typed_ast := TypedAst, icode := Icode} = string_to_code(ContractString, Options), {ok, {FunName, {fun_t, _, _, ArgTypes, RetType}}} = get_call_type(TypedAst), @@ -221,13 +223,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 +256,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, From bb728db51bc98b2046c77cc760c58ec0e4c4a303 Mon Sep 17 00:00:00 2001 From: Grzegorz Uriasz Date: Wed, 9 Sep 2020 18:17:21 +0200 Subject: [PATCH 11/11] Provide the ACI along with the bytecode --- src/aeso_aci.erl | 20 +++++++---- src/aeso_ast_infer_types.erl | 14 ++++---- src/aeso_compiler.erl | 70 ++++++++++++++++++++++-------------- test/aeso_aci_tests.erl | 10 ++++-- 4 files changed, 70 insertions(+), 44 deletions(-) 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 d85b3f6..bb09980 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -553,7 +553,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, []). @@ -563,7 +563,7 @@ 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)}), @@ -576,15 +576,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() diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 64f3f98..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,61 @@ 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 + , 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 = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, Options), #{ fcode => Fcode - , typed_ast => TypedAst + , unfolded_typed_ast => UnfoldedTypedAst + , folded_typed_ast => FoldedTypedAst , type_env => TypeEnv , ast => Ast } end. @@ -203,7 +219,7 @@ check_call1(ContractString0, FunName, Args, Options) -> %% First check the contract without the __call function #{ast := Ast} = string_to_code(ContractString0, Options), ContractString = insert_call_function(Ast, ContractString0, ?CALL_NAME, FunName, Args), - #{typed_ast := TypedAst, + #{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 ], @@ -313,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]), @@ -389,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/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. -