From 1d9f59fec3089fd4e2eac3647d5af36589e5c7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Rowicki?= <35342116+radrow@users.noreply.github.com> Date: Tue, 18 May 2021 12:21:57 +0200 Subject: [PATCH] Contract factories and bytecode introspection (#305) * Support for CREATE, CLONE and BYTECODE_HASH * Add missing files * Pushed the clone example through the typechecker * CLONE compiles * Fix dependent type in CLONE * Bytecode hash fixes * Refactor * Refactor 2 * move some logic away * Fixed some error messages. Type inference of child contract still does some random shit\n(mistakes arguments with result type) * CREATE sometimes compiles and sometimes not * Fix some scoping/constraint issues * works, needs cleanup * cleanup * Fix some tests. Remove optimization of singleton tuples * Fix default argument for clone * Cleanup * CHANGELOG * Mention void type * Address review, fix some dialyzer errors * Please dialyzer * Fix failing tests * Write negative tests * Docs * TOC * missing 'the' * missing 'the' * missing 'the' * missing 'the' * mention pre-fund * format * pre-fund clarification * format * Grammar in docs --- CHANGELOG.md | 9 + docs/sophia.md | 214 ++++++----- docs/sophia_stdlib.md | 121 ++++++ priv/stdlib/List.aes | 9 +- rebar.config | 2 +- rebar.lock | 2 +- src/aeso_aci.erl | 20 +- src/aeso_ast_infer_types.erl | 344 +++++++++++++++--- src/aeso_ast_to_fcode.erl | 170 ++++++--- src/aeso_ast_to_icode.erl | 6 +- src/aeso_code_errors.erl | 12 +- src/aeso_compiler.erl | 11 +- src/aeso_fcode_to_fate.erl | 135 ++++--- src/aeso_parser.erl | 16 +- src/aeso_pretty.erl | 10 +- src/aeso_scan.erl | 4 +- src/aeso_syntax.erl | 7 +- src/aeso_utils.hrl | 6 + test/aeso_abi_tests.erl | 2 +- test/aeso_aci_tests.erl | 97 ++--- test/aeso_calldata_tests.erl | 4 +- test/aeso_compiler_tests.erl | 94 +++-- test/aeso_parser_tests.erl | 4 +- test/contracts/__call.aes | 2 +- test/contracts/address_chain.aes | 4 +- test/contracts/address_literals.aes | 2 +- test/contracts/ambiguous_main.aes | 5 + test/contracts/bad_address_literals.aes | 2 +- test/contracts/bad_protected_call.aes | 2 +- test/contracts/bad_top_level_decl.aes | 2 +- test/contracts/clone.aes | 28 ++ test/contracts/clone_simple.aes | 7 + .../code_errors/bad_aens_resolve.aes | 2 +- .../code_errors/child_with_decls.aes | 5 + .../code_errors/higher_order_map_keys.aes | 2 +- .../code_errors/higher_order_query_type.aes | 2 +- .../higher_order_response_type.aes | 2 +- .../last_declaration_must_be_contract.aes | 2 - .../code_errors/missing_definition.aes | 2 +- .../code_errors/polymorphic_aens_resolve.aes | 2 +- .../code_errors/polymorphic_map_keys.aes | 2 +- .../code_errors/polymorphic_query_type.aes | 2 +- .../code_errors/polymorphic_response_type.aes | 2 +- .../code_errors/unapplied_contract_call.aes | 2 +- .../unapplied_named_arg_builtin.aes | 2 +- test/contracts/complex_types.aes | 2 +- test/contracts/contract_as_namespace.aes | 2 +- test/contracts/create.aes | 28 ++ test/contracts/environment.aes | 2 +- test/contracts/events.aes | 2 +- test/contracts/factorial.aes | 2 +- test/contracts/factories_type_errors.aes | 24 ++ test/contracts/identity.aes | 5 +- test/contracts/interface_with_defs.aes | 5 + test/contracts/lhs_matching.aes | 2 +- test/contracts/missing_event_type.aes | 2 +- test/contracts/multiple_contracts.aes | 10 +- test/contracts/multiple_main_contracts.aes | 5 + test/contracts/no_main_contract.aes | 2 + test/contracts/non_functional_entrypoint.aes | 2 +- test/contracts/protected_call.aes | 2 +- test/contracts/remote_call.aes | 10 +- test/contracts/spend_test.aes | 2 +- test/contracts/state_handling.aes | 2 +- test/contracts/stateful.aes | 2 +- test/contracts/test.aes | 108 +----- test/contracts/type_clash.aes | 2 +- test/contracts/unapplied_builtins.aes | 2 +- 68 files changed, 1113 insertions(+), 494 deletions(-) create mode 100644 src/aeso_utils.hrl create mode 100644 test/contracts/ambiguous_main.aes create mode 100644 test/contracts/clone.aes create mode 100644 test/contracts/clone_simple.aes create mode 100644 test/contracts/code_errors/child_with_decls.aes delete mode 100644 test/contracts/code_errors/last_declaration_must_be_contract.aes create mode 100644 test/contracts/create.aes create mode 100644 test/contracts/factories_type_errors.aes create mode 100644 test/contracts/interface_with_defs.aes create mode 100644 test/contracts/multiple_main_contracts.aes create mode 100644 test/contracts/no_main_contract.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index 188389f..5aa5a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Child contracts +- `Chain.clone` +- `Chain.create` +- `Chain.bytecode_hash` +- Minor support for variadic functions +- `void` type that represents an empty type ### Changed +- Contract interfaces must be now invocated by `contract interface` keywords +- `main` keyword to indicate the main contract in case there are child contracts around +- `List.sum` and `List.product` no longer use `List.foldl` ### Removed ## [5.0.0] 2021-04-30 diff --git a/docs/sophia.md b/docs/sophia.md index 98f0a43..8f8fd42 100644 --- a/docs/sophia.md +++ b/docs/sophia.md @@ -1,65 +1,6 @@ -**Table of Contents** - -- [-](#-) -- [Language Features](#language-features) - - [Contracts](#contracts) - - [Calling other contracts](#calling-other-contracts) - - [Protected contract calls](#protected-contract-calls) - - [Mutable state](#mutable-state) - - [Stateful functions](#stateful-functions) - - [Payable](#payable) - - [Payable contracts](#payable-contracts) - - [Payable entrypoints](#payable-entrypoints) - - [Namespaces](#namespaces) - - [Splitting code over multiple files](#splitting-code-over-multiple-files) - - [Standard library](#standard-library) - - [Types](#types) - - [Literals](#literals) - - [Arithmetic](#arithmetic) - - [Bit fields](#bit-fields) - - [Type aliases](#type-aliases) - - [Algebraic data types](#algebraic-data-types) - - [Lists](#lists) - - [Maps and records](#maps-and-records) - - [Constructing maps and records](#constructing-maps-and-records) - - [Accessing values](#accessing-values) - - [Updating a value](#updating-a-value) - - [Map implementation](#map-implementation) - - [Strings](#strings) - - [Chars](#chars) - - [Byte arrays](#byte-arrays) - - [Cryptographic builins](#cryptographic-builins) - - [AEVM note](#aevm-note) - - [Authorization interface](#authorization-interface) - - [Oracle interface](#oracle-interface) - - [Example](#example) - - [Sanity checks](#sanity-checks) - - [AENS interface](#aens-interface) - - [Example](#example-1) - - [Events](#events) - - [Argument order](#argument-order) - - [Compiler pragmas](#compiler-pragmas) - - [Exceptions](#exceptions) - - [Syntax](#syntax) - - [Lexical syntax](#lexical-syntax) - - [Comments](#comments) - - [Keywords](#keywords) - - [Tokens](#tokens) - - [Layout blocks](#layout-blocks) - - [Notation](#notation) - - [Declarations](#declarations) - - [Types](#types-1) - - [Statements](#statements) - - [Expressions](#expressions) - - [Operators types](#operators-types) - - [Operator precendences](#operator-precendences) - - [Examples](#examples) - - [Delegation signature](#delegation-signature) - - -## The Sophia Language +# The Sophia Language An Æternity BlockChain Language The Sophia is a language in the ML family. It is strongly typed and has @@ -69,6 +10,65 @@ Sophia is customized for smart contracts, which can be published to a blockchain (the Æternity BlockChain). Thus some features of conventional languages, such as floating point arithmetic, are not present in Sophia, and some blockchain specific primitives, constructions and types have been added. + +**Table of Contents** + + - [Language Features](#language-features) + - [Contracts](#contracts) + - [Calling other contracts](#calling-other-contracts) + - [Protected contract calls](#protected-contract-calls) + - [Contract factories and child contracts](#contract-factories-and-child-contracts) + - [Mutable state](#mutable-state) + - [Stateful functions](#stateful-functions) + - [Payable](#payable) + - [Payable contracts](#payable-contracts) + - [Payable entrypoints](#payable-entrypoints) + - [Namespaces](#namespaces) + - [Splitting code over multiple files](#splitting-code-over-multiple-files) + - [Standard library](#standard-library) + - [Types](#types) + - [Literals](#literals) + - [Arithmetic](#arithmetic) + - [Bit fields](#bit-fields) + - [Type aliases](#type-aliases) + - [Algebraic data types](#algebraic-data-types) + - [Lists](#lists) + - [Maps and records](#maps-and-records) + - [Constructing maps and records](#constructing-maps-and-records) + - [Accessing values](#accessing-values) + - [Updating a value](#updating-a-value) + - [Map implementation](#map-implementation) + - [Strings](#strings) + - [Chars](#chars) + - [Byte arrays](#byte-arrays) + - [Cryptographic builins](#cryptographic-builins) + - [AEVM note](#aevm-note) + - [Authorization interface](#authorization-interface) + - [Oracle interface](#oracle-interface) + - [Example](#example) + - [Sanity checks](#sanity-checks) + - [AENS interface](#aens-interface) + - [Example](#example) + - [Events](#events) + - [Argument order](#argument-order) + - [Compiler pragmas](#compiler-pragmas) + - [Exceptions](#exceptions) + - [Syntax](#syntax) + - [Lexical syntax](#lexical-syntax) + - [Comments](#comments) + - [Keywords](#keywords) + - [Tokens](#tokens) + - [Layout blocks](#layout-blocks) + - [Notation](#notation) + - [Declarations](#declarations) + - [Types](#types) + - [Statements](#statements) + - [Expressions](#expressions) + - [Operators types](#operators-types) + - [Operator precendences](#operator-precendences) + - [Examples](#examples) + - [Delegation signature](#delegation-signature) + ## Language Features ### Contracts @@ -94,16 +94,16 @@ To call a function in another contract you need the address to an instance of the contract. The type of the address must be a contract type, which consists of a number of type definitions and entrypoint declarations. For instance, -```javascript +```sophia // A contract type -contract VotingType = +contract interface VotingType = entrypoint vote : string => unit ``` Now given contract address of type `VotingType` you can call the `vote` entrypoint of that contract: -```javascript +```sophia contract VoteTwice = entrypoint voteTwice(v : VotingType, alt : string) = v.vote(alt) @@ -114,7 +114,7 @@ Contract calls take two optional named arguments `gas : int` and `value : int` that lets you set a gas limit and provide tokens to a contract call. If omitted the defaults are no gas limit and no tokens. Suppose there is a fee for voting: -```javascript +```sophia entrypoint voteTwice(v : VotingType, fee : int, alt : string) = v.vote(value = fee, alt) v.vote(value = fee, alt) @@ -136,7 +136,7 @@ To recover the underlying `address` of a contract instance there is a field `address : address`. For instance, to send tokens to the voting contract (given that it is payable) without calling it you can write -```javascript +```sophia entrypoint pay(v : VotingType, amount : int) = Chain.spend(v.address, amount) ``` @@ -154,7 +154,7 @@ If the call fails the result is `None`, otherwise it's `Some(r)` where `r` is the return value of the call. ```sophia -contract VotingType = +contract interface VotingType = entrypoint : vote : string => unit contract Voter = @@ -171,10 +171,42 @@ However, note that errors that would normally consume all the gas in the transaction still only uses up the gas spent running the contract. +#### Contract factories and child contracts + +Since the version 5.0.0 Sophia supports deploying contracts by other +contracts. This can be done in two ways: + +- Contract cloning via [`Chain.clone`](sophia_stdlib.md#clone) +- Direct deploy via [`Chain.create`](sophia_stdlib.md#create) + +These functions take variable number of arguments that must match the created +contract's `init` function. Beside that they take some additional named +arguments – please refer to their documentation for the details. + +While `Chain.clone` requires only a `contract interface` and a living instance +of a given contract on the chain, `Chain.create` needs a full definition of a +to-create contract defined by the standard `contract` syntax, for example + +``` +contract IntHolder = + type state = int + entrypoint init(x) = x + entrypoint get() = state + +main contract IntHolderFactory = + entrypoint new(x : int) : IntHolder = + let ih = Chain.create(x) : IntHolder + ih +``` + +In case of a presence of child contracts (`IntHolder` in this case), the main +contract must be pointed out with the `main` keyword as shown in the example. + + ### Mutable state -Sophia does not have arbitrary mutable state, but only a limited form of -state associated with each contract instance. +Sophia does not have arbitrary mutable state, but only a limited form of state +associated with each contract instance. - Each contract defines a type `state` encapsulating its mutable state. The type `state` defaults to the `unit`. @@ -200,7 +232,7 @@ Top-level functions and entrypoints must be annotated with the `stateful` keyword to be allowed to affect the state of the running contract. For instance, -```javascript +```sophia stateful entrypoint set_state(s : state) = put(s) ``` @@ -237,7 +269,7 @@ A concrete contract is by default *not* payable. Any attempt at spending to such a contract (either a `Chain.spend` or a normal spend transaction) will fail. If a contract shall be able to receive funds in this way it has to be declared `payable`: -```javascript +```sophia // A payable contract payable contract ExampleContract = stateful entrypoint do_stuff() = ... @@ -253,7 +285,7 @@ A contract entrypoint is by default *not* payable. Any call to such a function that has a non-zero `value` will fail. Contract entrypoints that should be called with a non-zero value should be declared `payable`. -```javascript +```sophia payable stateful entrypoint buy(to : address) = if(Call.value > 42) transfer_item(to) @@ -414,7 +446,7 @@ corresponding integer, so setting very high bits can be expensive). Type aliases can be introduced with the `type` keyword and can be parameterized. For instance -``` +```sophia type number = int type string_map('a) = map(string, 'a) ``` @@ -434,7 +466,7 @@ datatype one_or_both('a, 'b) = Left('a) | Right('b) | Both('a, 'b) Elements of data types can be pattern matched against, using the `switch` construct: -``` +```sophia function get_left(x : one_or_both('a, 'b)) : option('a) = switch(x) Left(x) => Some(x) @@ -443,7 +475,7 @@ function get_left(x : one_or_both('a, 'b)) : option('a) = ``` or directly in the left-hand side: -``` +```sophia function get_left : one_or_both('a, 'b) => option('a) get_left(Left(x)) = Some(x) @@ -461,7 +493,7 @@ elements of a list can be any of datatype but they must have the same type. The type of lists with elements of type `'e` is written `list('e)`. For example we can have the following lists: -``` +```sophia [1, 33, 2, 666] : list(int) [(1, "aaa"), (10, "jjj"), (666, "the beast")] : list(int * string) [{[1] = "aaa", [10] = "jjj"}, {[5] = "eee", [666] = "the beast"}] : list(map(int, string)) @@ -475,13 +507,13 @@ and returns the resulting list. So concatenating two lists Sophia supports list comprehensions known from languages like Python, Haskell or Erlang. Example syntax: -``` +```sophia [x + y | x <- [1,2,3,4,5], let k = x*x, if (k > 5), y <- [k, k+1, k+2]] // yields [12,13,14,20,21,22,30,31,32] ``` Lists can be constructed using the range syntax using special `..` operator: -``` +```sophia [1..4] == [1,2,3,4] ``` The ranges are always ascending and have step equal to 1. @@ -493,7 +525,7 @@ Please refer to the [standard library](sophia_stdlib.md#List) for the predefined A Sophia record type is given by a fixed set of fields with associated, possibly different, types. For instance -``` +```sophia record account = { name : string, balance : int, history : list(transaction) } @@ -510,12 +542,12 @@ Please refer to the [standard library](sophia_stdlib.md#Map) for the predefined A value of record type is constructed by giving a value for each of the fields. For the example above, -``` +```sophia function new_account(name) = {name = name, balance = 0, history = []} ``` Maps are constructed similarly, with keys enclosed in square brackets -``` +```sophia function example_map() : map(string, int) = {["key1"] = 1, ["key2"] = 2} ``` @@ -524,7 +556,7 @@ The empty map is written `{}`. #### Accessing values Record fields access is written `r.f` and map lookup `m[k]`. For instance, -``` +```sophia function get_balance(a : address, accounts : map(address, account)) = accounts[a].balance ``` @@ -549,14 +581,14 @@ in the map or execution fails, but a default value can be provided: `k` is not in the map. Updates can be nested: -``` +```sophia function clear_history(a : address, accounts : map(address, account)) : map(address, account) = accounts{ [a].history = [] } ``` This is equivalent to `accounts{ [a] @ acc = acc{ history = [] } }` and thus requires `a` to be present in the accounts map. To have `clear_history` create an account if `a` is not in the map you can write (given a function `empty_account`): -``` +```sophia accounts{ [a = empty_account()].history = [] } ``` @@ -627,7 +659,7 @@ For a functionality documentation refer to the [standard library](sophia_stdlib. #### Example Example for an oracle answering questions of type `string` with answers of type `int`: -``` +```sophia contract Oracles = stateful entrypoint registerOracle(acct : address, @@ -698,7 +730,7 @@ an account with address `addr`. In order to allow a contract `ct` to handle Armed with this information we can for example write a function that extends the name if it expires within 1000 blocks: -``` +```sophia stateful entrypoint extend_if_necessary(addr : address, name : string, sig : signature) = switch(AENS.lookup(name)) None => () @@ -709,7 +741,7 @@ the name if it expires within 1000 blocks: And we can write functions that adds and removes keys from the pointers of the name: -``` +```sophia stateful entrypoint add_key(addr : address, name : string, key : string, pt : AENS.pointee, sig : signature) = switch(AENS.lookup(name)) @@ -768,7 +800,7 @@ The fields can appear in any order. Events are emitted by using the `Chain.event` function. The following function will emit one Event of each kind in the example. -``` +```sophia entrypoint emit_events() : () = Chain.event(TheFirstEvent(42)) Chain.event(AnotherEvent(Contract.address, "This is not indexed")) @@ -778,7 +810,7 @@ will emit one Event of each kind in the example. It is only possible to have one (1) `string` parameter in the event, but it can be placed in any position (and its value will end up in the `data` field), i.e. -``` +```sophia AnotherEvent(string, indexed address) ... @@ -837,7 +869,7 @@ and `*/` and can be nested. ``` contract elif else entrypoint false function if import include let mod namespace -private payable stateful switch true type record datatype +private payable stateful switch true type record datatype main interface ``` #### Tokens @@ -941,7 +973,7 @@ Args ::= '(' Sep(Pattern, ',') ')' Contract declarations must appear at the top-level. For example, -``` +```sophia contract Test = type t = int entrypoint add (x : t, y : t) = x + y @@ -1089,7 +1121,7 @@ In order of highest to lowest precedence. ## Examples -``` +```sophia /* * A simple crowd-funding example */ diff --git a/docs/sophia_stdlib.md b/docs/sophia_stdlib.md index a39c630..6710dcc 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -765,6 +765,127 @@ Chain.gas_limit : int The gas limit of the current block. +#### bytecode_hash +``` +Chain.bytecode_hash : 'c => option(hash) +``` + +Returns the hash of the contract's bytecode (or `None` if it is +nonexistent or deployed before FATE2). The type `'c` must be +instantiated with a contract. The charged gas increases linearly to +the size of the serialized bytecode of the deployed contract. + + +#### create +``` +Chain.create(value : int, ...) => 'c +``` + +Creates and deploys a new instance of a contract `'c`. All of the +unnamed arguments will be passed to the `init` function. The charged +gas increases linearly with the size of the compiled child contract's +bytecode. The `source_hash` on-chain entry of the newly created +contract will be the SHA256 hash over concatenation of + +- whole contract source code +- single null byte +- name of the child contract + +The resulting contract's public key can be predicted and in case it happens to +have some funds before its creation, its balance will be increased by +the `value` parameter. + +The `value` argument (default `0`) is equivalent to the value in the contract +creation transaction – it sets the initial value of the newly created contract +charging the calling contract. Note that this won't be visible in `Call.value` +in the `init` call of the new contract. It will be included in +`Contract.balance`, however. + + +The type `'c` must be instantiated with a contract. + + +Example usage: +``` +payable contract Auction = + record state = {supply: int, name: string} + entrypoint init(supply, name) = {supply: supply, name: name} + stateful payable entrypoint buy(amount) = + require(Call.value == amount, "amount_value_mismatch") + ... + stateful entrypoint sell(amount) = + require(amount >= 0, "negative_amount") + ... + +main contract Market = + type state = list(Auction) + entrypoint init() = [] + stateful entrypoint new(name : string) = + let auction = Chain.create(0, name) : Auction + put(new_auction::state) +``` + +The typechecker must be certain about the created contract's type, so it is +worth writing it explicitly as shown in the example. + +#### clone +``` +Chain.clone : ( ref : 'c, gas : int, value : int, protected : bool, ... + ) => if(protected) option('c) else 'c +``` + +Clones the contract under the mandatory named argument `ref`. That means a new +contract of the same bytecode and the same `payable` parameter shall be created. +**NOTE:** the `state` won't be copied and the contract will be initialized with +a regular call to the `init` function with the remaining unnamed arguments. The +resulting contract's public key can be predicted and in case it happens to have +some funds before its creation, its balance will be increased by the `value` +parameter. This operation is significantly cheaper than `Chain.create` as it +costs a fixed amount of gas. + + +The `gas` argument (default `Call.gas_left`) limits the gas supply for the +`init` call of the cloned contract. + +The `value` argument (default `0`) is equivalent to the value in the contract +creation transaction – it sets the initial value of the newly created contract +charging the calling contract. Note that this won't be visible in `Call.value` +in the `init` call of the new contract. It will be included in +`Contract.balance`, however. + +The `protected` argument (default `false`) works identically as in remote calls. +If set to `true` it will change the return type to `option('c)` and will catch +all errors such as `abort`, out of gas and wrong arguments. Note that it can +only take a boolean *literal*, so other expressions such as variables will be +rejected by the compiler. + +The type `'c` must be instantiated with a contract. + +Example usage: + +``` +payable contract interface Auction = + entrypoint init : (int, string) => void + stateful payable entrypoint buy : (int) => () + stateful entrypoint sell : (int) => () + +main contract Market = + type state = list(Auction) + entrypoint init() = [] + stateful entrypoint new_of(template : Auction, name : string) = + switch(Chain.clone(ref=template, protected=true, 0, name)) + None => abort("Bad auction!") + Some(new_auction) => + put(new_auction::state) +``` + +When cloning by an interface, `init` entrypoint declaration is required. It is a +good practice to set its return type to `void` in order to indicate that this +function is not supposed to be called and is state agnostic. Trivia: internal +implementation of the `init` function does not actually return `state`, but +calls `put` instead. Moreover, FATE prevents even handcrafted calls to `init`. + + #### event ``` Chain.event(e : event) : unit diff --git a/priv/stdlib/List.aes b/priv/stdlib/List.aes index faf853c..9493037 100644 --- a/priv/stdlib/List.aes +++ b/priv/stdlib/List.aes @@ -208,10 +208,13 @@ namespace List = [] => false h::t => if(p(h)) true else any(p, t) - function sum(l : list(int)) : int = foldl ((a, b) => a + b, 0, l) - - function product(l : list(int)) : int = foldl((a, b) => a * b, 1, l) + function sum(l : list(int)) : int = switch(l) + [] => 0 + h::t => h + sum(t) + function product(l : list(int)) : int = switch(l) + [] => 1 + h::t => h * sum(t) /** Zips two list by applying bimapping function on respective elements. * Drops the tail of the longer list. diff --git a/rebar.config b/rebar.config index 7107fa8..771f30e 100644 --- a/rebar.config +++ b/rebar.config @@ -2,7 +2,7 @@ {erl_opts, [debug_info]}. -{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"7f0d309"}}} +{deps, [ {aebytecode, {git, "https://github.com/aeternity/aebytecode.git", {ref,"951db9f"}}} , {getopt, "1.0.1"} , {eblake2, "1.0.0"} , {jsx, {git, "https://github.com/talentdeficit/jsx.git", diff --git a/rebar.lock b/rebar.lock index 54e0e24..477ca9f 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,7 +1,7 @@ {"1.1.0", [{<<"aebytecode">>, {git,"https://github.com/aeternity/aebytecode.git", - {ref,"7f0d3090d4dc6c4d5fca7645b0c21eb0e65ad208"}}, + {ref,"951db9f38412a1f798987403d24c512d82fec8c7"}}, 0}, {<<"aeserialization">>, {git,"https://github.com/aeternity/aeserialization.git", diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 24c6f49..5a1a509 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -21,6 +21,8 @@ , json_encode_expr/1 , json_encode_type/1]). +-include("aeso_utils.hrl"). + -type aci_type() :: json | string. -type json() :: jsx:json_term(). -type json_text() :: binary(). @@ -68,9 +70,7 @@ do_contract_interface(Type, Contract, Options) when is_binary(Contract) -> 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 | Options]), - %% io:format("~p\n", [TypedAst]), from_typed_ast(Type, TypedAst) catch throw:{error, Errors} -> {error, Errors} @@ -83,7 +83,7 @@ from_typed_ast(Type, TypedAst) -> string -> do_render_aci_json(JArray) end. -encode_contract(Contract = {contract, _, {con, _, Name}, _}) -> +encode_contract(Contract = {Head, _, {con, _, Name}, _}) when ?IS_CONTRACT_HEAD(Head) -> C0 = #{name => encode_name(Name)}, Tdefs0 = [ encode_typedef(T) || T <- sort_decls(contract_types(Contract)) ], @@ -107,7 +107,7 @@ encode_contract(Contract = {contract, _, {con, _, Name}, _}) -> || F <- sort_decls(contract_funcs(Contract)), is_entrypoint(F) ], - #{contract => C3#{functions => Fdefs, payable => is_payable(Contract)}}; + #{contract => C3#{kind => Head, functions => Fdefs, payable => is_payable(Contract)}}; encode_contract(Namespace = {namespace, _, {con, _, Name}, _}) -> Tdefs = [ encode_typedef(T) || T <- sort_decls(contract_types(Namespace)) ], #{namespace => #{name => encode_name(Name), @@ -232,13 +232,19 @@ do_render_aci_json(Json) -> {ok, list_to_binary(string:join(DecodedContracts, "\n"))}. decode_contract(#{contract := #{name := Name, + kind := Kind, payable := Payable, type_defs := Ts0, functions := Fs} = C}) -> MkTDef = fun(N, T) -> #{name => N, vars => [], typedef => T} end, Ts = [ MkTDef(<<"state">>, maps:get(state, C)) || maps:is_key(state, C) ] ++ [ MkTDef(<<"event">>, maps:get(event, C)) || maps:is_key(event, C) ] ++ Ts0, - [payable(Payable), "contract ", io_lib:format("~s", [Name])," =\n", + [payable(Payable), case Kind of + contract_main -> "main contract "; + contract_child -> "contract "; + contract_interface -> "contract interface " + end, + io_lib:format("~s", [Name])," =\n", decode_tdefs(Ts), decode_funcs(Fs)]; decode_contract(#{namespace := #{name := Name, type_defs := Ts}}) when Ts /= [] -> ["namespace ", io_lib:format("~s", [Name])," =\n", @@ -332,10 +338,10 @@ payable(false) -> "". %% #contract{Ann, Con, [Declarations]}. -contract_funcs({C, _, _, Decls}) when C == contract; C == namespace -> +contract_funcs({C, _, _, Decls}) when ?IS_CONTRACT_HEAD(C); C == namespace -> [ D || D <- Decls, is_fun(D)]. -contract_types({C, _, _, Decls}) when C == contract; C == namespace -> +contract_types({C, _, _, Decls}) when ?IS_CONTRACT_HEAD(C); C == namespace -> [ D || D <- Decls, is_type(D) ]. is_fun({letfun, _, _, _, _, _}) -> true; diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index 8755cb1..bd52d1b 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -18,7 +18,9 @@ , pp_type/2 ]). --type utype() :: {fun_t, aeso_syntax:ann(), named_args_t(), [utype()], utype()} +-include("aeso_utils.hrl"). + +-type utype() :: {fun_t, aeso_syntax:ann(), named_args_t(), [utype()] | var_args, utype()} | {app_t, aeso_syntax:ann(), utype(), [utype()]} | {tuple_t, aeso_syntax:ann(), [utype()]} | aeso_syntax:id() | aeso_syntax:qid() @@ -39,6 +41,7 @@ element(1, T) =:= qcon). -type why_record() :: aeso_syntax:field(aeso_syntax:expr()) + | {var_args, aeso_syntax:ann(), aeso_syntax:expr()} | {proj, aeso_syntax:ann(), aeso_syntax:expr(), aeso_syntax:id()}. -type pos() :: aeso_errors:pos(). @@ -73,7 +76,10 @@ -record(is_contract_constraint, { contract_t :: utype(), context :: {contract_literal, aeso_syntax:expr()} | - {address_to_contract, aeso_syntax:ann()} + {address_to_contract, aeso_syntax:ann()} | + {bytecode_hash, aeso_syntax:ann()} | + {var_args, aeso_syntax:ann(), aeso_syntax:expr()}, + force_def = false :: boolean() }). -type field_constraint() :: #field_constraint{} | #record_create_constraint{} | #is_contract_constraint{}. @@ -96,7 +102,7 @@ -type qname() :: [string()]. -type typesig() :: {type_sig, aeso_syntax:ann(), type_constraints(), [aeso_syntax:named_arg_t()], [type()], type()}. --type type_constraints() :: none | bytes_concat | bytes_split | address_to_contract. +-type type_constraints() :: none | bytes_concat | bytes_split | address_to_contract | bytecode_hash. -type fun_info() :: {aeso_syntax:ann(), typesig() | type()}. -type type_info() :: {aeso_syntax:ann(), typedef()}. @@ -123,13 +129,14 @@ , in_pattern = false :: boolean() , stateful = false :: boolean() , current_function = none :: none | aeso_syntax:id() - , what = top :: top | namespace | contract | main_contract + , what = top :: top | namespace | contract | contract_interface }). -type env() :: #env{}. -define(PRINT_TYPES(Fmt, Args), when_option(pp_types, fun () -> io:format(Fmt, Args) end)). +-define(CONSTRUCTOR_MOCK_NAME, "#__constructor__#"). %% -- Environment manipulation ----------------------------------------------- @@ -191,9 +198,9 @@ bind_fun(X, Type, Env) -> force_bind_fun(X, Type, Env = #env{ what = What }) -> Ann = aeso_syntax:get_ann(Type), NoCode = get_option(no_code, false), - Entry = if X == "init", What == main_contract, not NoCode -> + Entry = if X == "init", What == contract, not NoCode -> {reserved_init, Ann, Type}; - What == contract -> {contract_fun, Ann, Type}; + What == contract_interface -> {contract_fun, Ann, Type}; true -> {Ann, Type} end, on_current_scope(Env, fun(Scope = #scope{ funs = Funs }) -> @@ -261,13 +268,40 @@ contract_call_type({fun_t, Ann, [], Args, Ret}) -> Args, {if_t, Ann, Id("protected"), {app_t, Ann, {id, Ann, "option"}, [Ret]}, Ret}}. -spec bind_contract(aeso_syntax:decl(), env()) -> env(). -bind_contract({contract, Ann, Id, Contents}, Env) -> +bind_contract({Contract, Ann, Id, Contents}, Env) + when ?IS_CONTRACT_HEAD(Contract) -> Key = name(Id), Sys = [{origin, system}], - Fields = [ {field_t, AnnF, Entrypoint, contract_call_type(Type)} - || {fun_decl, AnnF, Entrypoint, Type} <- Contents ] ++ - %% Predefined fields - [ {field_t, Sys, {id, Sys, "address"}, {id, Sys, "address"}} ], + Fields = + [ {field_t, AnnF, Entrypoint, contract_call_type(Type)} + || {fun_decl, AnnF, Entrypoint, Type} <- Contents ] ++ + [ {field_t, AnnF, Entrypoint, + contract_call_type( + {fun_t, AnnF, [], [ArgT || {typed, _, _, ArgT} <- Args], RetT}) + } + || {letfun, AnnF, Entrypoint = {id, _, Name}, Args, _Type, {typed, _, _, RetT}} <- Contents, + Name =/= "init" + ] ++ + %% Predefined fields + [ {field_t, Sys, {id, Sys, "address"}, {id, Sys, "address"}} ] ++ + [ {field_t, Sys, {id, Sys, ?CONSTRUCTOR_MOCK_NAME}, + contract_call_type( + case [ [ArgT || {typed, _, _, ArgT} <- Args] + || {letfun, AnnF, {id, _, "init"}, Args, _, _} <- Contents, + aeso_syntax:get_ann(entrypoint, AnnF, false)] + ++ [ Args + || {fun_decl, AnnF, {id, _, "init"}, {fun_t, _, _, Args, _}} <- Contents, + aeso_syntax:get_ann(entrypoint, AnnF, false)] + ++ [ Args + || {fun_decl, AnnF, {id, _, "init"}, {type_sig, _, _, _, Args, _}} <- Contents, + aeso_syntax:get_ann(entrypoint, AnnF, false)] + of + [] -> {fun_t, [stateful,payable|Sys], [], [], {id, Sys, "void"}}; + [Args] -> {fun_t, [stateful,payable|Sys], [], Args, {id, Sys, "void"}} + end + ) + } + ], FieldInfo = [ {Entrypoint, #field_info{ ann = FieldAnn, kind = contract, field_t = Type, @@ -396,8 +430,11 @@ global_env() -> Map = fun(A, B) -> {app_t, Ann, {id, Ann, "map"}, [A, B]} end, Pair = fun(A, B) -> {tuple_t, Ann, [A, B]} end, FunC = fun(C, Ts, T) -> {type_sig, Ann, C, [], Ts, T} end, + FunC1 = fun(C, S, T) -> {type_sig, Ann, C, [], [S], T} end, Fun = fun(Ts, T) -> FunC(none, Ts, T) end, Fun1 = fun(S, T) -> Fun([S], T) end, + FunCN = fun(C, Named, Normal, Ret) -> {type_sig, Ann, C, Named, Normal, Ret} end, + FunN = fun(Named, Normal, Ret) -> FunCN(none, Named, Normal, Ret) end, %% Lambda = fun(Ts, T) -> {fun_t, Ann, [], Ts, T} end, %% Lambda1 = fun(S, T) -> Lambda([S], T) end, StateFun = fun(Ts, T) -> {type_sig, [stateful|Ann], none, [], Ts, T} end, @@ -424,6 +461,7 @@ global_env() -> TxFlds = [{"paying_for", Option(PayForTx)}, {"ga_metas", List(GAMetaTx)}, {"actor", Address}, {"fee", Int}, {"ttl", Int}, {"tx", BaseTx}], TxType = {record_t, [FldT(N, T) || {N, T} <- TxFlds ]}, + Stateful = fun(T) -> setelement(2, T, [stateful|element(2, T)]) end, Fee = Int, [A, Q, R, K, V] = lists:map(TVar, ["a", "q", "r", "k", "v"]), @@ -443,6 +481,7 @@ global_env() -> {"require", Fun([Bool, String], Unit)}]) , types = MkDefs( [{"int", 0}, {"bool", 0}, {"char", 0}, {"string", 0}, {"address", 0}, + {"void", 0}, {"unit", {[], {alias_t, Unit}}}, {"hash", {[], {alias_t, Bytes(32)}}}, {"signature", {[], {alias_t, Bytes(64)}}}, @@ -463,6 +502,23 @@ global_env() -> {"block_height", Int}, {"difficulty", Int}, {"gas_limit", Int}, + {"bytecode_hash",FunC1(bytecode_hash, A, Option(Hash))}, + {"create", Stateful( + FunN([ {named_arg_t, Ann, {id, Ann, "value"}, Int, {typed, Ann, {int, Ann, 0}, Int}} + ], var_args, A))}, + {"clone", Stateful( + FunN([ {named_arg_t, Ann, {id, Ann, "gas"}, Int, + {typed, Ann, + {app, Ann, + {typed, Ann, {qid, Ann, ["Call","gas_left"]}, + typesig_to_fun_t(Fun([], Int)) + }, + []}, Int + }} + , {named_arg_t, Ann, {id, Ann, "value"}, Int, {typed, Ann, {int, Ann, 0}, Int}} + , {named_arg_t, Ann, {id, Ann, "protected"}, Bool, {typed, Ann, {bool, Ann, false}, Bool}} + , {named_arg_t, Ann, {id, Ann, "ref"}, A, undefined} + ], var_args, A))}, %% Tx constructors {"GAMetaTx", Fun([Address, Int], GAMetaTx)}, {"PayingForTx", Fun([Address, Int], PayForTx)}, @@ -699,9 +755,13 @@ infer(Contracts, Options) -> try Env = init_env(Options), create_options(Options), + ets_new(defined_contracts, [bag]), ets_new(type_vars, [set]), check_modifiers(Env, Contracts), - {Env1, Decls} = infer1(Env, Contracts, [], Options), + create_type_errors(), + Contracts1 = identify_main_contract(Contracts, Options), + destroy_and_report_type_errors(Env), + {Env1, Decls} = infer1(Env, Contracts1, [], Options), {Env2, DeclsFolded, DeclsUnfolded} = case proplists:get_value(dont_unfold, Options, false) of true -> {Env1, Decls, Decls}; @@ -719,12 +779,21 @@ infer(Contracts, Options) -> -spec infer1(env(), [aeso_syntax:decl()], [aeso_syntax:decl()], list(option())) -> {env(), [aeso_syntax:decl()]}. infer1(Env, [], Acc, _Options) -> {Env, lists:reverse(Acc)}; -infer1(Env, [{contract, Ann, ConName, Code} | Rest], Acc, Options) -> +infer1(Env, [{Contract, Ann, ConName, Code} | Rest], Acc, Options) + when ?IS_CONTRACT_HEAD(Contract) -> %% do type inference on each contract independently. check_scope_name_clash(Env, contract, ConName), - What = if Rest == [] -> main_contract; true -> contract end, + What = case Contract of + contract_main -> contract; + contract_child -> contract; + contract_interface -> contract_interface + end, + case What of + contract -> ets_insert(defined_contracts, {qname(ConName)}); + contract_interface -> ok + end, {Env1, Code1} = infer_contract_top(push_scope(contract, ConName, Env), What, Code, Options), - Contract1 = {contract, Ann, ConName, Code1}, + Contract1 = {Contract, Ann, ConName, Code1}, Env2 = pop_scope(Env1), Env3 = bind_contract(Contract1, Env2), infer1(Env3, Rest, [Contract1 | Acc], Options); @@ -737,6 +806,25 @@ infer1(Env, [{pragma, _, _} | Rest], Acc, Options) -> %% Pragmas are checked in check_modifiers infer1(Env, Rest, Acc, Options). +%% Asserts that the main contract is somehow defined. +identify_main_contract(Contracts, Options) -> + Children = [C || C = {contract_child, _, _, _} <- Contracts], + Mains = [C || C = {contract_main, _, _, _} <- Contracts], + case Mains of + [] -> case Children of + [] -> type_error( + {main_contract_undefined, + [{file, File} || {src_file, File} <- Options]}); + [{contract_child, Ann, Con, Body}] -> + (Contracts -- Children) ++ [{contract_main, Ann, Con, Body}]; + [H|_] -> type_error({ambiguous_main_contract, + aeso_syntax:get_ann(H)}) + end; + [_] -> (Contracts -- Mains) ++ Mains; %% Move to the end + [H|_] -> type_error({multiple_main_contracts, + aeso_syntax:get_ann(H)}) + end. + check_scope_name_clash(Env, Kind, Name) -> case get_scope(Env, qname(Name)) of false -> ok; @@ -746,7 +834,7 @@ check_scope_name_clash(Env, Kind, Name) -> destroy_and_report_type_errors(Env) end. --spec infer_contract_top(env(), main_contract | contract | namespace, [aeso_syntax:decl()], list(option())) -> +-spec infer_contract_top(env(), contract_interface | contract | namespace, [aeso_syntax:decl()], list(option())) -> {env(), [aeso_syntax:decl()]}. infer_contract_top(Env, Kind, Defs0, Options) -> create_type_errors(), @@ -756,7 +844,7 @@ infer_contract_top(Env, Kind, Defs0, Options) -> %% infer_contract takes a proplist mapping global names to types, and %% a list of definitions. --spec infer_contract(env(), main_contract | contract | namespace, [aeso_syntax:decl()], list(option())) -> {env(), [aeso_syntax:decl()]}. +-spec infer_contract(env(), contract_interface | contract | namespace, [aeso_syntax:decl()], list(option())) -> {env(), [aeso_syntax:decl()]}. infer_contract(Env0, What, Defs0, Options) -> create_type_errors(), Defs01 = process_blocks(Defs0), @@ -772,19 +860,19 @@ infer_contract(Env0, What, Defs0, Options) -> ({fun_decl, _, _, _}) -> prototype; (_) -> unexpected end, - Get = fun(K) -> [ Def || Def <- Defs, Kind(Def) == K ] end, - {Env1, TypeDefs} = check_typedefs(Env, Get(type)), + Get = fun(K, In) -> [ Def || Def <- In, Kind(Def) == K ] end, + {Env1, TypeDefs} = check_typedefs(Env, Get(type, Defs)), create_type_errors(), - check_unexpected(Get(unexpected)), + check_unexpected(Get(unexpected, Defs)), Env2 = case What of - namespace -> Env1; - contract -> Env1; - main_contract -> bind_state(Env1) %% bind state and put + namespace -> Env1; + contract_interface -> Env1; + contract -> bind_state(Env1) %% bind state and put end, - {ProtoSigs, Decls} = lists:unzip([ check_fundecl(Env1, Decl) || Decl <- Get(prototype) ]), + {ProtoSigs, Decls} = lists:unzip([ check_fundecl(Env1, Decl) || Decl <- Get(prototype, Defs) ]), Env3 = bind_funs(ProtoSigs, Env2), - Functions = Get(function), + Functions = Get(function, Defs), %% Check for duplicates in Functions (we turn it into a map below) FunBind = fun({letfun, Ann, {id, _, Fun}, _, _, _}) -> {Fun, {tuple_t, Ann, []}}; ({fun_clauses, Ann, {id, _, Fun}, _, _}) -> {Fun, {tuple_t, Ann, []}} end, @@ -794,11 +882,11 @@ infer_contract(Env0, What, Defs0, Options) -> check_reserved_entrypoints(FunMap), DepGraph = maps:map(fun(_, Def) -> aeso_syntax_utils:used_ids(Def) end, FunMap), SCCs = aeso_utils:scc(DepGraph), - %% io:format("Dependency sorted functions:\n ~p\n", [SCCs]), {Env4, Defs1} = check_sccs(Env3, FunMap, SCCs, []), %% Check that `init` doesn't read or write the state check_state_dependencies(Env4, Defs1), destroy_and_report_type_errors(Env4), + %% Add inferred types of definitions {Env4, TypeDefs ++ Decls ++ Defs1}. %% Restructure blocks into multi-clause fundefs (`fun_clauses`). @@ -830,9 +918,9 @@ expose_internals(Defs, What) -> [ begin Ann = element(2, Def), NewAnn = case What of - namespace -> [A ||A <- Ann, A /= {private, true}, A /= private]; - main_contract -> [{entrypoint, true}|Ann]; % minor duplication - contract -> Ann + namespace -> [A ||A <- Ann, A /= {private, true}, A /= private]; + contract -> [{entrypoint, true}|Ann]; % minor duplication + contract_interface -> Ann end, Def1 = setelement(2, Def, NewAnn), case Def1 of % fix inner clauses @@ -907,15 +995,16 @@ check_modifiers(Env, Contracts) -> check_modifiers_(Env, Contracts), destroy_and_report_type_errors(Env). -check_modifiers_(Env, [{contract, _, Con, Decls} | Rest]) -> - IsMain = Rest == [], +check_modifiers_(Env, [{Contract, _, Con, Decls} | Rest]) + when ?IS_CONTRACT_HEAD(Contract) -> + IsInterface = Contract =:= contract_interface, check_modifiers1(contract, Decls), case {lists:keymember(letfun, 1, Decls), [ D || D <- Decls, aeso_syntax:get_ann(entrypoint, D, false) ]} of {true, []} -> type_error({contract_has_no_entrypoints, Con}); - _ when not IsMain -> - case [ {Ann, Id} || {letfun, Ann, Id, _, _, _} <- Decls ] of - [{Ann, Id} | _] -> type_error({definition_in_non_main_contract, Ann, Id}); + _ when IsInterface -> + case [ {AnnF, Id} || {letfun, AnnF, Id, _, _, _} <- Decls ] of + [{AnnF, Id} | _] -> type_error({definition_in_contract_interface, AnnF, Id}); [] -> ok end; _ -> ok @@ -1408,8 +1497,7 @@ infer_expr(Env, {typed, As, Body, Type}) -> {typed, _, NewBody, NewType} = check_expr(Env, Body, Type1), {typed, As, NewBody, NewType}; infer_expr(Env, {app, Ann, Fun, Args0} = App) -> - NamedArgs = [ Arg || Arg = {named_arg, _, _, _} <- Args0 ], - Args = Args0 -- NamedArgs, + {NamedArgs, Args} = split_args(Args0), case aeso_syntax:get_ann(format, Ann) of infix -> infer_op(Env, Ann, Fun, Args, fun infer_infix/1); @@ -1418,13 +1506,13 @@ infer_expr(Env, {app, Ann, Fun, Args0} = App) -> _ -> NamedArgsVar = fresh_uvar(Ann), NamedArgs1 = [ infer_named_arg(Env, NamedArgsVar, Arg) || Arg <- NamedArgs ], - %% TODO: named args constraints - NewFun={typed, _, _, FunType} = infer_expr(Env, Fun), + NewFun0 = infer_expr(Env, Fun), NewArgs = [infer_expr(Env, A) || A <- Args], ArgTypes = [T || {typed, _, _, T} <- NewArgs], + NewFun1 = {typed, _, _, FunType} = infer_var_args_fun(Env, NewFun0, NamedArgs1, ArgTypes), + When = {infer_app, Fun, NamedArgs1, Args, FunType, ArgTypes}, GeneralResultType = fresh_uvar(Ann), ResultType = fresh_uvar(Ann), - When = {infer_app, Fun, NamedArgs1, Args, FunType, ArgTypes}, unify(Env, FunType, {fun_t, [], NamedArgsVar, ArgTypes, GeneralResultType}, When), add_named_argument_constraint( #dependent_type_constraint{ named_args_t = NamedArgsVar, @@ -1432,7 +1520,7 @@ infer_expr(Env, {app, Ann, Fun, Args0} = App) -> general_type = GeneralResultType, specialized_type = ResultType, context = {check_return, App} }), - {typed, Ann, {app, Ann, NewFun, NamedArgs1 ++ NewArgs}, dereference(ResultType)} + {typed, Ann, {app, Ann, NewFun1, NamedArgs1 ++ NewArgs}, dereference(ResultType)} end; infer_expr(Env, {'if', Attrs, Cond, Then, Else}) -> NewCond = check_expr(Env, Cond, {id, Attrs, "bool"}), @@ -1537,6 +1625,62 @@ infer_expr(Env, Let = {letfun, Attrs, _, _, _, _}) -> type_error({missing_body_for_let, Attrs}), infer_expr(Env, {block, Attrs, [Let, abort_expr(Attrs, "missing body")]}). +infer_var_args_fun(Env, {typed, Ann, Fun, FunType0}, NamedArgs, ArgTypes) -> + FunType = + case Fun of + {qid, _, ["Chain", "create"]} -> + {fun_t, _, NamedArgsT, var_args, RetT} = FunType0, + GasCapMock = {named_arg_t, Ann, {id, Ann, "gas"}, {id, Ann, "int"}, {int, Ann, 0}}, + ProtectedMock = {named_arg_t, Ann, {id, Ann, "protected"}, {id, Ann, "bool"}, {bool, Ann, false}}, + NamedArgsT1 = case NamedArgsT of + [Value|Rest] -> [GasCapMock, Value, ProtectedMock|Rest]; + % generally type error, but will be caught + _ -> [GasCapMock, ProtectedMock|NamedArgsT] + end, + check_contract_construction(Env, true, RetT, Fun, NamedArgsT1, ArgTypes, RetT), + {fun_t, Ann, NamedArgsT, ArgTypes, RetT}; + {qid, _, ["Chain", "clone"]} -> + {fun_t, _, NamedArgsT, var_args, RetT} = FunType0, + ContractT = + case [ContractT || {named_arg, _, {id, _, "ref"}, {typed, _, _, ContractT}} <- NamedArgs] of + [C] -> C; + _ -> type_error({clone_no_contract, Ann}), + fresh_uvar(Ann) + end, + NamedArgsTNoRef = + lists:filter(fun({named_arg_t, _, {id, _, "ref"}, _, _}) -> false; (_) -> true end, NamedArgsT), + check_contract_construction(Env, false, ContractT, Fun, NamedArgsTNoRef, ArgTypes, RetT), + {fun_t, Ann, NamedArgsT, ArgTypes, + {if_t, Ann, {id, Ann, "protected"}, {app_t, Ann, {id, Ann, "option"}, [RetT]}, RetT}}; + _ -> FunType0 + end, + {typed, Ann, Fun, FunType}. + +-spec check_contract_construction(env(), boolean(), utype(), utype(), named_args_t(), [utype()], utype()) -> ok. +check_contract_construction(Env, ForceDef, ContractT, Fun, NamedArgsT, ArgTypes, RetT) -> + Ann = aeso_syntax:get_ann(Fun), + InitT = fresh_uvar(Ann), + unify(Env, InitT, {fun_t, Ann, NamedArgsT, ArgTypes, fresh_uvar(Ann)}, {checking_init_args, Ann, ContractT, ArgTypes}), + unify(Env, RetT, ContractT, {return_contract, Fun, ContractT}), + constrain( + [ #field_constraint{ + record_t = unfold_types_in_type(Env, ContractT), + field = {id, Ann, ?CONSTRUCTOR_MOCK_NAME}, + field_t = InitT, + kind = project, + context = {var_args, Ann, Fun} } + , #is_contract_constraint{ contract_t = ContractT, + context = {var_args, Ann, Fun}, + force_def = ForceDef + } + ]), + ok. + +split_args(Args0) -> + NamedArgs = [ Arg || Arg = {named_arg, _, _, _} <- Args0 ], + Args = Args0 -- NamedArgs, + {NamedArgs, Args}. + infer_named_arg(Env, NamedArgs, {named_arg, Ann, Id, E}) -> CheckedExpr = {typed, _, _, ArgType} = infer_expr(Env, E), check_stateful_named_arg(Env, Id, E), @@ -1704,7 +1848,7 @@ next_count() -> ets_tables() -> [options, type_vars, type_defs, record_fields, named_argument_constraints, - field_constraints, freshen_tvars, type_errors]. + field_constraints, freshen_tvars, type_errors, defined_contracts]. clean_up_ets() -> [ catch ets_delete(Tab) || Tab <- ets_tables() ], @@ -1814,7 +1958,7 @@ solve_named_argument_constraints(Env, Constraints0) -> [ C || C <- dereference_deep(Constraints0), unsolved == check_named_argument_constraint(Env, C) ]. -%% If false, a type error have been emitted, so it's safe to drop the constraint. +%% If false, a type error has been emitted, so it's safe to drop the constraint. -spec check_named_argument_constraint(env(), named_argument_constraint()) -> true | false | unsolved. check_named_argument_constraint(_Env, #named_argument_constraint{ args = {uvar, _, _} }) -> unsolved; @@ -1978,12 +2122,20 @@ check_record_create_constraints(Env, [C | Cs]) -> end, check_record_create_constraints(Env, Cs). +is_contract_defined(C) -> + ets_lookup(defined_contracts, qname(C)) =/= []. + check_is_contract_constraints(_Env, []) -> ok; check_is_contract_constraints(Env, [C | Cs]) -> - #is_contract_constraint{ contract_t = Type, context = Cxt } = C, + #is_contract_constraint{ contract_t = Type, context = Cxt, force_def = ForceDef } = C, Type1 = unfold_types_in_type(Env, instantiate(Type)), - case lookup_type(Env, record_type_name(Type1)) of - {_, {_Ann, {[], {contract_t, _}}}} -> ok; + TypeName = record_type_name(Type1), + case lookup_type(Env, TypeName) of + {_, {_Ann, {[], {contract_t, _}}}} -> + case not ForceDef orelse is_contract_defined(TypeName) of + true -> ok; + false -> type_error({contract_lacks_definition, Type1, Cxt}) + end; _ -> type_error({not_a_contract_type, Type1, Cxt}) end, check_is_contract_constraints(Env, Cs). @@ -2309,8 +2461,13 @@ unify1(_Env, {bytes_t, _, Len}, {bytes_t, _, Len}, _When) -> unify1(Env, {if_t, _, {id, _, Id}, Then1, Else1}, {if_t, _, {id, _, Id}, Then2, Else2}, When) -> unify(Env, Then1, Then2, When) andalso unify(Env, Else1, Else2, When); + +unify1(_Env, {fun_t, _, _, _, _}, {fun_t, _, _, var_args, _}, When) -> + type_error({unify_varargs, When}); +unify1(_Env, {fun_t, _, _, var_args, _}, {fun_t, _, _, _, _}, When) -> + type_error({unify_varargs, When}); unify1(Env, {fun_t, _, Named1, Args1, Result1}, {fun_t, _, Named2, Args2, Result2}, When) - when length(Args1) == length(Args2) -> + when length(Args1) == length(Args2) -> unify(Env, Named1, Named2, When) andalso unify(Env, Args1, Args2, When) andalso unify(Env, Result1, Result2, When); unify1(Env, {app_t, _, {Tag, _, F}, Args1}, {app_t, _, {Tag, _, F}, Args2}, When) @@ -2319,6 +2476,9 @@ unify1(Env, {app_t, _, {Tag, _, F}, Args1}, {app_t, _, {Tag, _, F}, Args2}, When unify1(Env, {tuple_t, _, As}, {tuple_t, _, Bs}, When) when length(As) == length(Bs) -> unify(Env, As, Bs, When); +unify1(Env, {named_arg_t, _, Id1, Type1, _}, {named_arg_t, _, Id2, Type2, _}, When) -> + unify1(Env, Id1, Id2, {arg_name, Id1, Id2, When}), + unify1(Env, Type1, Type2, When); %% The grammar is a bit inconsistent about whether types without %% arguments are represented as applications to an empty list of %% parameters or not. We therefore allow them to unify. @@ -2376,8 +2536,6 @@ occurs_check1(R, [H | T]) -> occurs_check(R, H) orelse occurs_check(R, T); occurs_check1(_, []) -> false. -fresh_uvar([{origin, system}]) -> - error(oh_no_you_dont); fresh_uvar(Attrs) -> {uvar, Attrs, make_ref()}. @@ -2426,7 +2584,11 @@ apply_typesig_constraint(Ann, address_to_contract, {fun_t, _, [], [_], Type}) -> apply_typesig_constraint(Ann, bytes_concat, {fun_t, _, [], [A, B], C}) -> add_bytes_constraint({add_bytes, Ann, concat, A, B, C}); apply_typesig_constraint(Ann, bytes_split, {fun_t, _, [], [C], {tuple_t, _, [A, B]}}) -> - add_bytes_constraint({add_bytes, Ann, split, A, B, C}). + add_bytes_constraint({add_bytes, Ann, split, A, B, C}); +apply_typesig_constraint(Ann, bytecode_hash, {fun_t, _, _, [Con], _}) -> + constrain([#is_contract_constraint{ contract_t = Con, + context = {bytecode_hash, Ann} }]). + %% Dereferences all uvars and replaces the uninstantiated ones with a %% succession of tvars. @@ -2555,6 +2717,9 @@ mk_error({not_a_contract_type, Type, Cxt}) -> end, {Pos, Cxt1} = case Cxt of + {var_args, Ann, Fun} -> + {pos(Ann), + io_lib:format("when calling variadic function\n~s\n", [pp_expr(" ", Fun)])}; {contract_literal, Lit} -> {pos(Lit), io_lib:format("when checking that the contract literal\n~s\n" @@ -2653,7 +2818,7 @@ mk_error({namespace, _Pos, {con, Pos, Name}, _Def}) -> Msg = io_lib:format("Nested namespaces are not allowed\nNamespace '~s' at ~s not defined at top level.\n", [Name, pp_loc(Pos)]), mk_t_err(pos(Pos), Msg); -mk_error({contract, _Pos, {con, Pos, Name}, _Def}) -> +mk_error({Contract, _Pos, {con, Pos, Name}, _Def}) when ?IS_CONTRACT_HEAD(Contract) -> Msg = io_lib:format("Nested contracts are not allowed\nContract '~s' at ~s not defined at top level.\n", [Name, pp_loc(Pos)]), mk_t_err(pos(Pos), Msg); @@ -2728,8 +2893,8 @@ mk_error({contract_has_no_entrypoints, Con}) -> "contract functions must be declared with the 'entrypoint' keyword instead of\n" "'function'.\n", [pp_expr("", Con), pp_loc(Con)]), mk_t_err(pos(Con), Msg); -mk_error({definition_in_non_main_contract, Ann, {id, _, Id}}) -> - Msg = "Only the main contract can contain defined functions or entrypoints.\n", +mk_error({definition_in_contract_interface, Ann, {id, _, Id}}) -> + Msg = "Contract interfaces cannot contain defined functions or entrypoints.\n", Cxt = io_lib:format("Fix: replace the definition of '~s' by a type signature.\n", [Id]), mk_t_err(pos(Ann), Msg, Cxt); mk_error({unbound_type, Type}) -> @@ -2798,6 +2963,29 @@ mk_error({named_argument_must_be_literal_bool, Name, Arg}) -> mk_error({conflicting_updates_for_field, Upd, Key}) -> Msg = io_lib:format("Conflicting updates for field '~s'\n", [Key]), mk_t_err(pos(Upd), Msg); +mk_error({ambiguous_main_contract, Ann}) -> + Msg = "Could not deduce the main contract. You can point it out manually with the `main` keyword.", + mk_t_err(pos(Ann), Msg); +mk_error({main_contract_undefined, Ann}) -> + Msg = "No contract defined.\n", + mk_t_err(pos(Ann), Msg); +mk_error({multiple_main_contracts, Ann}) -> + Msg = "Only one main contract can be defined.\n", + mk_t_err(pos(Ann), Msg); +mk_error({unify_varargs, When}) -> + Msg = "Cannot unify variable argument list.\n", + {Pos, Ctxt} = pp_when(When), + mk_t_err(Pos, Msg, Ctxt); +mk_error({clone_no_contract, Ann}) -> + Msg = "Chain.clone requires `ref` named argument of contract type.\n", + mk_t_err(pos(Ann), Msg); +mk_error({contract_lacks_definition, Type, When}) -> + Msg = io_lib:format( + "~s is not implemented.\n", + [pp_type(Type)] + ), + {Pos, Ctxt} = pp_when(When), + mk_t_err(Pos, Msg, Ctxt); mk_error(Err) -> Msg = io_lib:format("Unknown error: ~p\n", [Err]), mk_t_err(pos(0, 0), Msg). @@ -2833,6 +3021,12 @@ pp_when({field_constraint, FieldType0, InferredType0, Fld}) -> InferredType = instantiate(InferredType0), {pos(Fld), case Fld of + {var_args, _Ann, _Fun} -> + io_lib:format("when checking contract construction of type\n~s (at ~s)\nagainst the expected type\n~s\n", + [pp_type(" ", FieldType), + pp_loc(Fld), + pp_type(" ", InferredType) + ]); {field, _Ann, LV, Id, E} -> io_lib:format("when checking the assignment of the field\n~s (at ~s)\nto the old value ~s and the new value\n~s\n", [pp_typed(" ", {lvalue, [], LV}, FieldType), @@ -2855,6 +3049,13 @@ pp_when({record_constraint, RecType0, InferredType0, Fld}) -> InferredType = instantiate(InferredType0), {Pos, WhyRec} = pp_why_record(Fld), case Fld of + {var_args, _Ann, _Fun} -> + {Pos, + io_lib:format("when checking that contract construction of type\n~s\n~s\n" + "matches the expected type\n~s\n", + [pp_type(" ", RecType), WhyRec, pp_type(" ", InferredType)] + ) + }; {field, _Ann, _LV, _Id, _E} -> {Pos, io_lib:format("when checking that the record type\n~s\n~s\n" @@ -2908,17 +3109,42 @@ pp_when({check_named_arg_constraint, C}) -> Err = io_lib:format("when checking named argument\n~s\nagainst inferred type\n~s", [pp_typed(" ", Arg, Type), pp_type(" ", C#named_argument_constraint.type)]), {pos(Arg), Err}; +pp_when({checking_init_args, Ann, Con0, ArgTypes0}) -> + Con = instantiate(Con0), + ArgTypes = instantiate(ArgTypes0), + {pos(Ann), + io_lib:format("when checking arguments of ~s's init entrypoint to match\n(~s)", + [pp_type(Con), string:join([pp_type(A) || A <- ArgTypes], ", ")]) + }; +pp_when({return_contract, App, Con0}) -> + Con = instantiate(Con0), + {pos(App) + , io_lib:format("when checking that expression returns contract of type\n~s", [pp_type(" ", Con)]) + }; +pp_when({arg_name, Id1, Id2, When}) -> + {Pos, Ctx} = pp_when(When), + {Pos + , io_lib:format("when unifying names of named arguments: ~s and ~s\n~s", [pp_expr(Id1), pp_expr(Id2), Ctx]) + }; +pp_when({var_args, Ann, Fun}) -> + {pos(Ann) + , io_lib:format("when resolving arguments of variadic function\n~s\n", [pp_expr(" ", Fun)]) + }; pp_when(unknown) -> {pos(0,0), ""}. -spec pp_why_record(why_record()) -> {pos(), iolist()}. -pp_why_record(Fld = {field, _Ann, LV, _Id, _E}) -> - {pos(Fld), - io_lib:format("arising from an assignment of the field ~s (at ~s)", - [pp_expr("", {lvalue, [], LV}), pp_loc(Fld)])}; +pp_why_record({var_args, Ann, Fun}) -> + {pos(Ann), + io_lib:format("arising from resolution of variadic function ~s (at ~s)", + [pp_expr(Fun), pp_loc(Fun)])}; pp_why_record(Fld = {field, _Ann, LV, _E}) -> {pos(Fld), io_lib:format("arising from an assignment of the field ~s (at ~s)", - [pp_expr("", {lvalue, [], LV}), pp_loc(Fld)])}; + [pp_expr({lvalue, [], LV}), pp_loc(Fld)])}; +pp_why_record(Fld = {field, _Ann, LV, _Alias, _E}) -> + {pos(Fld), + io_lib:format("arising from an assignment of the field ~s (at ~s)", + [pp_expr({lvalue, [], LV}), pp_loc(Fld)])}; pp_why_record({proj, _Ann, Rec, FldName}) -> {pos(Rec), io_lib:format("arising from the projection of the field ~s (at ~s)", @@ -2938,9 +3164,13 @@ pp_typed(Label, {typed, _, Expr, _}, Type) -> pp_typed(Label, Expr, Type) -> pp_expr(Label, {typed, [], Expr, Type}). +pp_expr(Expr) -> + pp_expr("", Expr). pp_expr(Label, Expr) -> prettypr:format(prettypr:beside(prettypr:text(Label), aeso_pretty:expr(Expr, [show_generated]))). +pp_type(Type) -> + pp_type("", Type). pp_type(Label, Type) -> prettypr:format(prettypr:beside(prettypr:text(Label), aeso_pretty:type(Type, [show_generated]))). diff --git a/src/aeso_ast_to_fcode.erl b/src/aeso_ast_to_fcode.erl index 07f4e01..835d4d0 100644 --- a/src/aeso_ast_to_fcode.erl +++ b/src/aeso_ast_to_fcode.erl @@ -12,6 +12,8 @@ -export([ast_to_fcode/2, format_fexpr/1]). -export_type([fcode/0, fexpr/0, fun_def/0]). +-include("aeso_utils.hrl"). + %% -- Type definitions ------------------------------------------------------- -type option() :: term(). @@ -53,6 +55,7 @@ | {oracle_pubkey, binary()} | {oracle_query_id, binary()} | {bool, false | true} + | {contract_code, string()} %% for CREATE, by name | {typerep, ftype()}. -type fexpr() :: {lit, flit()} @@ -136,24 +139,27 @@ -type type_env() :: #{ sophia_name() => type_def() }. -type fun_env() :: #{ sophia_name() => {fun_name(), non_neg_integer()} }. -type con_env() :: #{ sophia_name() => con_tag() }. --type builtins() :: #{ sophia_name() => {builtin(), non_neg_integer() | none} }. +-type child_con_env() :: #{sophia_name() => fcode()}. +-type builtins() :: #{ sophia_name() => {builtin(), non_neg_integer() | none | variable} }. --type context() :: {main_contract, string()} +-type context() :: {contract_def, string()} | {namespace, string()} | {abstract_contract, string()}. -type state_layout() :: {tuple, [state_layout()]} | {reg, state_reg()}. --type env() :: #{ type_env := type_env(), - fun_env := fun_env(), - con_env := con_env(), - event_type => aeso_syntax:typedef(), - builtins := builtins(), - options := [option()], - state_layout => state_layout(), - context => context(), - vars => [var_name()], - functions := #{ fun_name() => fun_def() } }. +-type env() :: #{ type_env := type_env(), + fun_env := fun_env(), + con_env := con_env(), + child_con_env := child_con_env(), + event_type => aeso_syntax:typedef(), + builtins := builtins(), + options := [option()], + state_layout => state_layout(), + context => context(), + vars => [var_name()], + functions := #{ fun_name() => fun_def() } + }. -define(HASH_BYTES, 32). @@ -161,17 +167,26 @@ %% Main entrypoint. Takes typed syntax produced by aeso_ast_infer_types:infer/1,2 %% and produces Fate intermediate code. --spec ast_to_fcode(aeso_syntax:ast(), [option()]) -> fcode(). +-spec ast_to_fcode(aeso_syntax:ast(), [option()]) -> {env(), fcode()}. ast_to_fcode(Code, Options) -> - Verbose = lists:member(pp_fcode, Options), init_fresh_names(), - FCode1 = to_fcode(init_env(Options), Code), + {Env1, FCode1} = to_fcode(init_env(Options), Code), + FCode2 = optimize(FCode1, Options), + Env2 = Env1#{ child_con_env := + maps:map( + fun (_, FC) -> optimize(FC, Options) end, + maps:get(child_con_env, Env1) + )}, + clear_fresh_names(), + {Env2, FCode2}. + +optimize(FCode1, Options) -> + Verbose = lists:member(pp_fcode, Options), [io:format("-- Before lambda lifting --\n~s\n\n", [format_fcode(FCode1)]) || Verbose], FCode2 = optimize_fcode(FCode1), [ io:format("-- After optimization --\n~s\n\n", [format_fcode(FCode2)]) || Verbose, FCode2 /= FCode1 ], FCode3 = lambda_lift(FCode2), [ io:format("-- After lambda lifting --\n~s\n\n", [format_fcode(FCode3)]) || Verbose, FCode3 /= FCode2 ], - clear_fresh_names(), FCode3. %% -- Environment ------------------------------------------------------------ @@ -182,6 +197,7 @@ init_env(Options) -> #{ type_env => init_type_env(), fun_env => #{}, builtins => builtins(), + child_con_env => #{}, con_env => #{["None"] => #con_tag{ tag = 0, arities = [0, 1] }, ["Some"] => #con_tag{ tag = 1, arities = [0, 1] }, ["RelativeTTL"] => #con_tag{ tag = 0, arities = [1, 1] }, @@ -217,7 +233,8 @@ init_env(Options) -> ["Chain", "GAAttachTx"] => #con_tag{ tag = 21, arities = ChainTxArities } }, options => Options, - functions => #{} }. + functions => #{} + }. -spec builtins() -> builtins(). builtins() -> @@ -227,7 +244,7 @@ builtins() -> Scopes = [{[], [{"abort", 1}, {"require", 2}]}, {["Chain"], [{"spend", 2}, {"balance", 1}, {"block_hash", 1}, {"coinbase", none}, {"timestamp", none}, {"block_height", none}, {"difficulty", none}, - {"gas_limit", none}]}, + {"gas_limit", none}, {"bytecode_hash", 1}, {"create", variable}, {"clone", variable}]}, {["Contract"], [{"address", none}, {"balance", none}, {"creator", none}]}, {["Call"], [{"origin", none}, {"caller", none}, {"value", none}, {"gas_price", none}, {"gas_left", 0}]}, @@ -307,31 +324,44 @@ get_option(Opt, Env, Default) -> %% -- Compilation ------------------------------------------------------------ --spec to_fcode(env(), aeso_syntax:ast()) -> fcode(). -to_fcode(Env, [{contract, Attrs, MainCon = {con, _, Main}, Decls}]) -> - #{ builtins := Builtins } = Env, - MainEnv = Env#{ context => {main_contract, Main}, - builtins => Builtins#{[Main, "state"] => {get_state, none}, - [Main, "put"] => {set_state, 1}, - [Main, "Chain", "event"] => {chain_event, 1}} }, - #{ functions := Funs } = Env1 = - decls_to_fcode(MainEnv, Decls), - StateType = lookup_type(Env1, [Main, "state"], [], {tuple, []}), - EventType = lookup_type(Env1, [Main, "event"], [], none), - StateLayout = state_layout(Env1), - Payable = proplists:get_value(payable, Attrs, false), - #{ contract_name => Main, - state_type => StateType, - state_layout => StateLayout, - event_type => EventType, - payable => Payable, - functions => add_init_function(Env1, MainCon, StateType, - add_event_function(Env1, EventType, Funs)) }; -to_fcode(_Env, [NotContract]) -> - fcode_error({last_declaration_must_be_contract, NotContract}); -to_fcode(Env, [{contract, _, {con, _, Con}, Decls} | Code]) -> - Env1 = decls_to_fcode(Env#{ context => {abstract_contract, Con} }, Decls), - to_fcode(Env1, Code); +-spec to_fcode(env(), aeso_syntax:ast()) -> {env(), fcode()}. +to_fcode(Env, [{Contract, Attrs, Con = {con, _, Name}, Decls}|Rest]) + when ?IS_CONTRACT_HEAD(Contract) -> + case Contract =:= contract_interface of + false -> + #{ builtins := Builtins } = Env, + ConEnv = Env#{ context => {contract_def, Name}, + builtins => Builtins#{[Name, "state"] => {get_state, none}, + [Name, "put"] => {set_state, 1}, + [Name, "Chain", "event"] => {chain_event, 1}} }, + #{ functions := PrevFuns } = ConEnv, + #{ functions := Funs } = Env1 = + decls_to_fcode(ConEnv, Decls), + StateType = lookup_type(Env1, [Name, "state"], [], {tuple, []}), + EventType = lookup_type(Env1, [Name, "event"], [], none), + StateLayout = state_layout(Env1), + Payable = proplists:get_value(payable, Attrs, false), + ConFcode = #{ contract_name => Name, + state_type => StateType, + state_layout => StateLayout, + event_type => EventType, + payable => Payable, + functions => add_init_function( + Env1, Con, StateType, + add_event_function(Env1, EventType, Funs)) }, + case Contract of + contract_main -> [] = Rest, {Env1, ConFcode}; + contract_child -> + Env2 = add_child_con(Env1, Name, ConFcode), + Env3 = Env2#{ functions := PrevFuns }, + to_fcode(Env3, Rest) + end; + true -> + Env1 = decls_to_fcode(Env#{ context => {abstract_contract, Name} }, Decls), + to_fcode(Env1, Rest) + end; +to_fcode(_Env, [NotMain = {NotMainHead, _ ,_ , _}]) when NotMainHead =/= contract_def -> + fcode_error({last_declaration_must_be_contract_def, NotMain}); to_fcode(Env, [{namespace, _, {con, _, Con}, Decls} | Code]) -> Env1 = decls_to_fcode(Env#{ context => {namespace, Con} }, Decls), to_fcode(Env1, Code). @@ -341,13 +371,11 @@ decls_to_fcode(Env, Decls) -> %% First compute mapping from Sophia names to fun_names and add it to the %% environment. Env1 = add_fun_env(Env, Decls), - lists:foldl(fun(D, E) -> - R = decl_to_fcode(E, D), - R + lists:foldl(fun(D, E) -> decl_to_fcode(E, D) end, Env1, Decls). -spec decl_to_fcode(env(), aeso_syntax:decl()) -> env(). -decl_to_fcode(Env = #{context := {main_contract, _}}, {fun_decl, _, Id, _}) -> +decl_to_fcode(Env = #{context := {contract_def, _}}, {fun_decl, _, Id, _}) -> case is_no_code(Env) of false -> fcode_error({missing_definition, Id}); true -> Env @@ -410,7 +438,7 @@ typedef_to_fcode(Env, Id = {id, _, Name}, Xs, Def) -> Env3 = compute_state_layout(Env2, Name, FDef), bind_type(Env3, Q, FDef). -compute_state_layout(Env = #{ context := {main_contract, _} }, "state", Type) -> +compute_state_layout(Env = #{ context := {contract_def, _} }, "state", Type) -> NoLayout = get_option(no_flatten_state, Env), Layout = case Type([]) of @@ -436,7 +464,7 @@ compute_state_layout(R, [H | T]) -> compute_state_layout(R, _) -> {R + 1, {reg, R}}. -check_state_and_event_types(#{ context := {main_contract, _} }, Id, [_ | _]) -> +check_state_and_event_types(#{ context := {contract_def, _} }, Id, [_ | _]) -> case Id of {id, _, "state"} -> fcode_error({parameterized_state, Id}); {id, _, "event"} -> fcode_error({parameterized_event, Id}); @@ -461,8 +489,12 @@ type_to_fcode(Env, Sub, {record_t, Fields}) -> type_to_fcode(Env, Sub, {tuple_t, [], lists:map(FieldType, Fields)}); type_to_fcode(_Env, _Sub, {bytes_t, _, N}) -> {bytes, N}; +type_to_fcode(_Env, _Sub, {tvar, Ann, "void"}) -> + fcode_error({found_void, Ann}); type_to_fcode(_Env, Sub, {tvar, _, X}) -> maps:get(X, Sub, {tvar, X}); +type_to_fcode(_Env, _Sub, {fun_t, Ann, _, var_args, _}) -> + fcode_error({var_args_not_set, {id, Ann, "a very suspicious function"}}); type_to_fcode(Env, Sub, {fun_t, _, Named, Args, Res}) -> FNamed = [type_to_fcode(Env, Sub, Arg) || {named_arg_t, _, _, Arg, _} <- Named], FArgs = [type_to_fcode(Env, Sub, Arg) || Arg <- Args], @@ -678,14 +710,31 @@ expr_to_fcode(Env, _Type, {app, _Ann, {Op, _}, [A]}) when is_atom(Op) -> end; %% Function calls -expr_to_fcode(Env, _Type, {app, _, Fun = {typed, _, _, {fun_t, _, NamedArgsT, _, _}}, Args}) -> +expr_to_fcode(Env, Type, {app, _, Fun = {typed, _, FunE, {fun_t, _, NamedArgsT, ArgsT, _}}, Args}) -> Args1 = get_named_args(NamedArgsT, Args), FArgs = [expr_to_fcode(Env, Arg) || Arg <- Args1], case expr_to_fcode(Env, Fun) of {builtin_u, B, _Ar, TypeArgs} -> builtin_to_fcode(state_layout(Env), B, FArgs ++ TypeArgs); + {builtin_u, chain_clone, _Ar} -> + case ArgsT of + var_args -> fcode_error({var_args_not_set, FunE}); + _ -> + %% Here we little cheat on the typechecker, but this inconsistency + %% is to be solved in `aeso_fcode_to_fate:type_to_scode/1` + FInitArgsT = aeb_fate_data:make_typerep([type_to_fcode(Env, T) || T <- ArgsT]), + builtin_to_fcode(state_layout(Env), chain_clone, [{lit, FInitArgsT}|FArgs]) + end; + {builtin_u, chain_create, _Ar} -> + case {ArgsT, Type} of + {var_args, _} -> fcode_error({var_args_not_set, FunE}); + {_, {con, _, Contract}} -> + FInitArgsT = aeb_fate_data:make_typerep([type_to_fcode(Env, T) || T <- ArgsT]), + builtin_to_fcode(state_layout(Env), chain_create, [{lit, {contract_code, Contract}}, {lit, FInitArgsT}|FArgs]); + {_, _} -> fcode_error({not_a_contract_type, Type}) + end; {builtin_u, B, _Ar} -> builtin_to_fcode(state_layout(Env), B, FArgs); {def_u, F, _Ar} -> {def, F, FArgs}; - {remote_u, ArgsT, RetT, Ct, RFun} -> {remote, ArgsT, RetT, Ct, RFun, FArgs}; + {remote_u, RArgsT, RRetT, Ct, RFun} -> {remote, RArgsT, RRetT, Ct, RFun, FArgs}; FFun -> %% FFun is a closure, with first component the function name and %% second component the environment @@ -1614,6 +1663,10 @@ bind_constructors(Env = #{ con_env := ConEnv }, NewCons) -> %% -- Names -- +-spec add_child_con(env(), sophia_name(), fcode()) -> env(). +add_child_con(Env = #{child_con_env := CEnv}, Name, Fcode) -> + Env#{ child_con_env := CEnv#{Name => Fcode} }. + -spec add_fun_env(env(), [aeso_syntax:decl()]) -> env(). add_fun_env(Env = #{ context := {abstract_contract, _} }, _) -> Env; %% no functions from abstract contracts add_fun_env(Env = #{ fun_env := FunEnv }, Decls) -> @@ -1628,7 +1681,7 @@ add_fun_env(Env = #{ fun_env := FunEnv }, Decls) -> make_fun_name(#{ context := Context }, Ann, Name) -> Entrypoint = proplists:get_value(entrypoint, Ann, false), case Context of - {main_contract, Main} -> + {contract_def, Main} -> if Entrypoint -> {entrypoint, list_to_binary(Name)}; true -> {local_fun, [Main, Name]} end; @@ -1640,7 +1693,7 @@ make_fun_name(#{ context := Context }, Ann, Name) -> current_namespace(#{ context := Cxt }) -> case Cxt of {abstract_contract, Con} -> Con; - {main_contract, Con} -> Con; + {contract_def, Con} -> Con; {namespace, NS} -> NS end. @@ -1987,8 +2040,11 @@ internal_error(Error) -> %% -- Pretty printing -------------------------------------------------------- format_fcode(#{ functions := Funs }) -> - prettypr:format(pp_above( - [ pp_fun(Name, Def) || {Name, Def} <- maps:to_list(Funs) ])). + prettypr:format(format_funs(Funs)). + +format_funs(Funs) -> + pp_above( + [ pp_fun(Name, Def) || {Name, Def} <- maps:to_list(Funs) ]). format_fexpr(E) -> prettypr:format(pp_fexpr(E)). @@ -2105,7 +2161,9 @@ pp_fexpr({set_state, R, A}) -> pp_call(pp_text("set_state"), [{lit, {int, R}}, A]); pp_fexpr({get_state, R}) -> pp_call(pp_text("get_state"), [{lit, {int, R}}]); -pp_fexpr({switch, Split}) -> pp_split(Split). +pp_fexpr({switch, Split}) -> pp_split(Split); +pp_fexpr({contract_code, Contract}) -> + pp_beside(pp_text("contract "), pp_text(Contract)). pp_call(Fun, Args) -> pp_beside(Fun, pp_fexpr({tuple, Args})). diff --git a/src/aeso_ast_to_icode.erl b/src/aeso_ast_to_icode.erl index b1a1874..8cace86 100644 --- a/src/aeso_ast_to_icode.erl +++ b/src/aeso_ast_to_icode.erl @@ -14,12 +14,13 @@ -include_lib("aebytecode/include/aeb_opcodes.hrl"). -include("aeso_icode.hrl"). +-include("aeso_utils.hrl"). -spec convert_typed(aeso_syntax:ast(), list()) -> aeso_icode:icode(). convert_typed(TypedTree, Options) -> {Payable, Name} = case lists:last(TypedTree) of - {contract, Attrs, {con, _, Con}, _} -> + {Contr, Attrs, {con, _, Con}, _} when ?IS_CONTRACT_HEAD(Contr) -> {proplists:get_value(payable, Attrs, false), Con}; Decl -> gen_error({last_declaration_must_be_contract, Decl}) @@ -29,7 +30,8 @@ convert_typed(TypedTree, Options) -> Icode = code(TypedTree, NewIcode, Options), deadcode_elimination(Icode). -code([{contract, _Attribs, Con, Code}|Rest], Icode, Options) -> +code([{Contract, _Attribs, Con, Code}|Rest], Icode, Options) + when ?IS_CONTRACT_HEAD(Contract) -> NewIcode = contract_to_icode(Code, aeso_icode:set_namespace(Con, Icode)), code(Rest, NewIcode, Options); code([{namespace, _Ann, Name, Code}|Rest], Icode, Options) -> diff --git a/src/aeso_code_errors.erl b/src/aeso_code_errors.erl index 05bc135..b3d1308 100644 --- a/src/aeso_code_errors.erl +++ b/src/aeso_code_errors.erl @@ -10,9 +10,9 @@ -export([format/1, pos/1]). -format({last_declaration_must_be_contract, Decl = {namespace, _, {con, _, C}, _}}) -> - Msg = io_lib:format("Expected a contract as the last declaration instead of the namespace '~s'\n", - [C]), +format({last_declaration_must_be_main_contract, Decl = {Kind, _, {con, _, C}, _}}) -> + Msg = io_lib:format("Expected a main contract as the last declaration instead of the ~p '~s'\n", + [Kind, C]), mk_err(pos(Decl), Msg); format({missing_init_function, Con}) -> Msg = io_lib:format("Missing init function for the contract '~s'.\n", [pp_expr(Con)]), @@ -87,6 +87,12 @@ format({higher_order_state, {type_def, Ann, _, _, State}}) -> Msg = io_lib:format("Invalid state type\n~s\n", [pp_type(2, State)]), Cxt = "The state cannot contain functions in the AEVM. Use FATE if you need this.\n", mk_err(pos(Ann), Msg, Cxt); +format({var_args_not_set, Expr}) -> + mk_err( pos(Expr), "Could not deduce type of variable arguments list" + , "When compiling " ++ pp_expr(Expr) + ); +format({found_void, Ann}) -> + mk_err(pos(Ann), "Found a void-typed value.", "`void` is a restricted, uninhabited type. Did you mean `unit`?"); format(Err) -> mk_err(aeso_errors:pos(0, 0), io_lib:format("Unknown error: ~p\n", [Err])). diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 25cb1b9..4a69687 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -28,6 +28,7 @@ -include_lib("aebytecode/include/aeb_opcodes.hrl"). -include("aeso_icode.hrl"). +-include("aeso_utils.hrl"). -type option() :: pp_sophia_code @@ -137,8 +138,9 @@ from_string1(aevm, ContractString, Options) -> {ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}; from_string1(fate, ContractString, Options) -> #{ fcode := FCode + , fcode_env := #{child_con_env := ChildContracts} , folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options), - FateCode = aeso_fcode_to_fate:compile(FCode, Options), + FateCode = aeso_fcode_to_fate:compile(ChildContracts, FCode, Options), pp_assembler(fate, FateCode, Options), ByteCode = aeb_fate_code:serialize(FateCode, []), {ok, Version} = version(), @@ -178,8 +180,9 @@ string_to_code(ContractString, Options) -> , type_env => TypeEnv , ast => Ast }; fate -> - Fcode = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, Options), + {Env, Fcode} = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, [{original_src, ContractString}|Options]), #{ fcode => Fcode + , fcode_env => Env , unfolded_typed_ast => UnfoldedTypedAst , folded_typed_ast => FoldedTypedAst , type_env => TypeEnv @@ -468,7 +471,7 @@ error_missing_call_function() -> Msg = "Internal error: missing '__call'-function", aeso_errors:throw(aeso_errors:new(internal_error, Msg)). -get_call_type([{contract, _, _, Defs}]) -> +get_call_type([{Contract, _, _, Defs}]) when ?IS_CONTRACT_HEAD(Contract) -> case [ {lists:last(QFunName), FunType} || {letfun, _, {id, _, ?CALL_NAME}, [], _Ret, {typed, _, @@ -482,7 +485,7 @@ get_call_type([_ | Contracts]) -> get_call_type(Contracts). -dialyzer({nowarn_function, get_decode_type/2}). -get_decode_type(FunName, [{contract, Ann, _, Defs}]) -> +get_decode_type(FunName, [{Contract, Ann, _, Defs}]) when ?IS_CONTRACT_HEAD(Contract) -> GetType = fun({letfun, _, {id, _, Name}, Args, Ret, _}) when Name == FunName -> [{Args, Ret}]; ({fun_decl, _, {id, _, Name}, {fun_t, _, _, Args, Ret}}) when Name == FunName -> [{Args, Ret}]; (_) -> [] end, diff --git a/src/aeso_fcode_to_fate.erl b/src/aeso_fcode_to_fate.erl index 623b2ec..e93da66 100644 --- a/src/aeso_fcode_to_fate.erl +++ b/src/aeso_fcode_to_fate.erl @@ -9,7 +9,7 @@ %%%------------------------------------------------------------------- -module(aeso_fcode_to_fate). --export([compile/2, term_to_fate/1]). +-export([compile/2, compile/3, term_to_fate/1, term_to_fate/2]). -ifdef(TEST). -export([optimize_fun/4, to_basic_blocks/1]). @@ -45,7 +45,7 @@ -define(s(N), {store, N}). -define(void, {var, 9999}). --record(env, { contract, vars = [], locals = [], current_function, tailpos = true }). +-record(env, { contract, vars = [], locals = [], current_function, tailpos = true, child_contracts = #{}, options = []}). %% -- Debugging -------------------------------------------------------------- @@ -70,9 +70,11 @@ code_error(Err) -> %% @doc Main entry point. compile(FCode, Options) -> + compile(#{}, FCode, Options). +compile(ChildContracts, FCode, Options) -> #{ contract_name := ContractName, functions := Functions } = FCode, - SFuns = functions_to_scode(ContractName, Functions, Options), + SFuns = functions_to_scode(ChildContracts, ContractName, Functions, Options), SFuns1 = optimize_scode(SFuns, Options), FateCode = to_basic_blocks(SFuns1), ?debug(compile, Options, "~s\n", [aeb_fate_asm:pp(FateCode)]), @@ -85,19 +87,20 @@ make_function_name(event) -> <<"Chain.event">>; make_function_name({entrypoint, Name}) -> Name; make_function_name({local_fun, Xs}) -> list_to_binary("." ++ string:join(Xs, ".")). -functions_to_scode(ContractName, Functions, Options) -> +functions_to_scode(ChildContracts, ContractName, Functions, Options) -> FunNames = maps:keys(Functions), maps:from_list( - [ {make_function_name(Name), function_to_scode(ContractName, FunNames, Name, Attrs, Args, Body, Type, Options)} + [ {make_function_name(Name), function_to_scode(ChildContracts, ContractName, FunNames, Name, Attrs, Args, Body, Type, Options)} || {Name, #{args := Args, body := Body, attrs := Attrs, return := Type}} <- maps:to_list(Functions)]). -function_to_scode(ContractName, Functions, Name, Attrs0, Args, Body, ResType, _Options) -> +function_to_scode(ChildContracts, ContractName, Functions, Name, Attrs0, Args, Body, ResType, Options) -> {ArgTypes, ResType1} = typesig_to_scode(Args, ResType), Attrs = Attrs0 -- [stateful], %% Only track private and payable from here. - SCode = to_scode(init_env(ContractName, Functions, Name, Args), Body), + Env = init_env(ChildContracts, ContractName, Functions, Name, Args, Options), + SCode = to_scode(Env, Body), {Attrs, {ArgTypes, ResType1}, SCode}. -define(tvars, '$tvars'). @@ -133,7 +136,9 @@ type_to_scode({tvar, X}) -> put(?tvars, {I + 1, Vars#{ X => I }}), {tvar, I}; J -> {tvar, J} - end. + end; +type_to_scode(L) when is_list(L) -> {tuple, types_to_scode(L)}. + types_to_scode(Ts) -> lists:map(fun type_to_scode/1, Ts). @@ -142,11 +147,13 @@ types_to_scode(Ts) -> lists:map(fun type_to_scode/1, Ts). %% -- Environment functions -- -init_env(ContractName, FunNames, Name, Args) -> +init_env(ChildContracts, ContractName, FunNames, Name, Args, Options) -> #env{ vars = [ {X, {arg, I}} || {I, {X, _}} <- with_ixs(Args) ], contract = ContractName, + child_contracts = ChildContracts, locals = FunNames, current_function = Name, + options = Options, tailpos = true }. next_var(#env{ vars = Vars }) -> @@ -169,7 +176,7 @@ lookup_var(#env{vars = Vars}, X) -> %% -- The compiler -- -lit_to_fate(L) -> +lit_to_fate(Env, L) -> case L of {int, N} -> aeb_fate_data:make_integer(N); {string, S} -> aeb_fate_data:make_string(S); @@ -179,63 +186,80 @@ lit_to_fate(L) -> {contract_pubkey, K} -> aeb_fate_data:make_contract(K); {oracle_pubkey, K} -> aeb_fate_data:make_oracle(K); {oracle_query_id, H} -> aeb_fate_data:make_oracle_query(H); + {contract_code, C} -> + Options = Env#env.options, + FCode = maps:get(C, Env#env.child_contracts), + FateCode = compile(Env#env.child_contracts, FCode, Options), + ByteCode = aeb_fate_code:serialize(FateCode, []), + {ok, Version} = aeso_compiler:version(), + OriginalSourceCode = proplists:get_value(original_src, Options, ""), + Code = #{byte_code => ByteCode, + compiler_version => Version, + source_hash => crypto:hash(sha256, OriginalSourceCode ++ [0] ++ C), + type_info => [], + abi_version => aeb_fate_abi:abi_version(), + payable => maps:get(payable, FCode) + }, + Serialized = aeser_contract_code:serialize(Code), + aeb_fate_data:make_contract_bytearray(Serialized); {typerep, T} -> aeb_fate_data:make_typerep(type_to_scode(T)) end. -term_to_fate(E) -> term_to_fate(#{}, E). +term_to_fate(E) -> term_to_fate(#env{}, #{}, E). +term_to_fate(GlobEnv, E) -> term_to_fate(GlobEnv, #{}, E). -term_to_fate(_Env, {lit, L}) -> - lit_to_fate(L); +term_to_fate(GlobEnv, _Env, {lit, L}) -> + lit_to_fate(GlobEnv, L); %% negative literals are parsed as 0 - N -term_to_fate(_Env, {op, '-', [{lit, {int, 0}}, {lit, {int, N}}]}) -> +term_to_fate(_GlobEnv, _Env, {op, '-', [{lit, {int, 0}}, {lit, {int, N}}]}) -> aeb_fate_data:make_integer(-N); -term_to_fate(_Env, nil) -> +term_to_fate(_GlobEnv, _Env, nil) -> aeb_fate_data:make_list([]); -term_to_fate(Env, {op, '::', [Hd, Tl]}) -> +term_to_fate(GlobEnv, Env, {op, '::', [Hd, Tl]}) -> %% The Tl will translate into a list, because FATE lists are just lists - [term_to_fate(Env, Hd) | term_to_fate(Env, Tl)]; -term_to_fate(Env, {tuple, As}) -> - aeb_fate_data:make_tuple(list_to_tuple([ term_to_fate(Env, A) || A<-As])); -term_to_fate(Env, {con, Ar, I, As}) -> - FateAs = [ term_to_fate(Env, A) || A <- As ], + [term_to_fate(GlobEnv, Env, Hd) | term_to_fate(GlobEnv, Env, Tl)]; +term_to_fate(GlobEnv, Env, {tuple, As}) -> + aeb_fate_data:make_tuple(list_to_tuple([ term_to_fate(GlobEnv, Env, A) || A<-As])); +term_to_fate(GlobEnv, Env, {con, Ar, I, As}) -> + FateAs = [ term_to_fate(GlobEnv, Env, A) || A <- As ], aeb_fate_data:make_variant(Ar, I, list_to_tuple(FateAs)); -term_to_fate(_Env, {builtin, bits_all, []}) -> +term_to_fate(_GlobEnv, _Env, {builtin, bits_all, []}) -> aeb_fate_data:make_bits(-1); -term_to_fate(_Env, {builtin, bits_none, []}) -> +term_to_fate(_GlobEnv, _Env, {builtin, bits_none, []}) -> aeb_fate_data:make_bits(0); -term_to_fate(_Env, {op, bits_set, [B, I]}) -> - {bits, N} = term_to_fate(B), - J = term_to_fate(I), +term_to_fate(GlobEnv, _Env, {op, bits_set, [B, I]}) -> + {bits, N} = term_to_fate(GlobEnv, B), + J = term_to_fate(GlobEnv, I), {bits, N bor (1 bsl J)}; -term_to_fate(_Env, {op, bits_clear, [B, I]}) -> - {bits, N} = term_to_fate(B), - J = term_to_fate(I), +term_to_fate(GlobEnv, _Env, {op, bits_clear, [B, I]}) -> + {bits, N} = term_to_fate(GlobEnv, B), + J = term_to_fate(GlobEnv, I), {bits, N band bnot (1 bsl J)}; -term_to_fate(Env, {'let', X, E, Body}) -> - Env1 = Env#{ X => term_to_fate(Env, E) }, - term_to_fate(Env1, Body); -term_to_fate(Env, {var, X}) -> +term_to_fate(GlobEnv, Env, {'let', X, E, Body}) -> + Env1 = Env#{ X => term_to_fate(GlobEnv, Env, E) }, + term_to_fate(GlobEnv, Env1, Body); +term_to_fate(_GlobEnv, Env, {var, X}) -> case maps:get(X, Env, undefined) of undefined -> throw(not_a_fate_value); V -> V end; -term_to_fate(_Env, {builtin, map_empty, []}) -> +term_to_fate(_GlobEnv, _Env, {builtin, map_empty, []}) -> aeb_fate_data:make_map(#{}); -term_to_fate(Env, {op, map_set, [M, K, V]}) -> - Map = term_to_fate(Env, M), - Map#{term_to_fate(Env, K) => term_to_fate(Env, V)}; -term_to_fate(_Env, _) -> +term_to_fate(GlobEnv, Env, {op, map_set, [M, K, V]}) -> + Map = term_to_fate(GlobEnv, Env, M), + Map#{term_to_fate(GlobEnv, Env, K) => term_to_fate(GlobEnv, Env, V)}; +term_to_fate(_GlobEnv, _Env, _) -> throw(not_a_fate_value). to_scode(Env, T) -> - try term_to_fate(T) of + try term_to_fate(Env, T) of V -> [push(?i(V))] catch throw:not_a_fate_value -> to_scode1(Env, T) end. -to_scode1(_Env, {lit, L}) -> - [push(?i(lit_to_fate(L)))]; +to_scode1(Env, {lit, L}) -> + [push(?i(lit_to_fate(Env, L)))]; to_scode1(_Env, nil) -> [aeb_fate_ops:nil(?a)]; @@ -555,7 +579,27 @@ builtin_to_scode(Env, aens_lookup, [_Name] = Args) -> builtin_to_scode(_Env, auth_tx_hash, []) -> [aeb_fate_ops:auth_tx_hash(?a)]; builtin_to_scode(_Env, auth_tx, []) -> - [aeb_fate_ops:auth_tx(?a)]. + [aeb_fate_ops:auth_tx(?a)]; +builtin_to_scode(Env, chain_bytecode_hash, [_Addr] = Args) -> + call_to_scode(Env, aeb_fate_ops:bytecode_hash(?a, ?a), Args); +builtin_to_scode(Env, chain_clone, + [InitArgsT, GasCap, Value, Prot, Contract | InitArgs]) -> + case GasCap of + {builtin, call_gas_left, _} -> + call_to_scode(Env, aeb_fate_ops:clone(?a, ?a, ?a, ?a), + [Contract, InitArgsT, Value, Prot | InitArgs] + ); + _ -> + call_to_scode(Env, aeb_fate_ops:clone_g(?a, ?a, ?a, ?a, ?a), + [Contract, InitArgsT, Value, GasCap, Prot | InitArgs] + ) + end; + +builtin_to_scode(Env, chain_create, + [ Code, InitArgsT, Value | InitArgs]) -> + call_to_scode(Env, aeb_fate_ops:create(?a, ?a, ?a), + [Code, InitArgsT, Value | InitArgs] + ). %% -- Operators -- @@ -941,6 +985,10 @@ attributes(I) -> {'STR_TO_LOWER', A, B} -> Pure(A, [B]); {'CHAR_TO_INT', A, B} -> Pure(A, [B]); {'CHAR_FROM_INT', A, B} -> Pure(A, [B]); + {'CREATE', A, B, C} -> Impure(?a, [A, B, C]); + {'CLONE', A, B, C, D} -> Impure(?a, [A, B, C, D]); + {'CLONE_G', A, B, C, D, E} -> Impure(?a, [A, B, C, D, E]); + {'BYTECODE_HASH', A, B} -> Impure(A, [B]); {'ABORT', A} -> Impure(pc, A); {'EXIT', A} -> Impure(pc, A); 'NOP' -> Pure(none, []) @@ -1694,6 +1742,9 @@ split_calls(Ref, [I | Code], Acc, Blocks) when element(1, I) == 'CALL'; element(1, I) == 'CALL_R'; element(1, I) == 'CALL_GR'; element(1, I) == 'CALL_PGR'; + element(1, I) == 'CREATE'; + element(1, I) == 'CLONE'; + element(1, I) == 'CLONE_G'; element(1, I) == 'jumpif' -> split_calls(make_ref(), Code, [], [{Ref, lists:reverse([I | Acc])} | Blocks]); split_calls(Ref, [{'ABORT', _} = I | _Code], Acc, Blocks) -> diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index acb09c6..c29403d 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -93,8 +93,20 @@ decl() -> ?LAZY_P( choice( %% Contract declaration - [ ?RULE(keyword(contract), con(), tok('='), maybe_block(decl()), {contract, _1, _2, _4}) - , ?RULE(token(payable), keyword(contract), con(), tok('='), maybe_block(decl()), add_modifiers([_1], {contract, _2, _3, _5})) + [ ?RULE(token(main), keyword(contract), + con(), tok('='), maybe_block(decl()), {contract_main, _2, _3, _5}) + , ?RULE(keyword(contract), + con(), tok('='), maybe_block(decl()), {contract_child, _1, _2, _4}) + , ?RULE(keyword(contract), token(interface), + con(), tok('='), maybe_block(decl()), {contract_interface, _1, _3, _5}) + , ?RULE(token(payable), token(main), keyword(contract), + con(), tok('='), maybe_block(decl()), add_modifiers([_1], {contract_main, _3, _4, _6})) + , ?RULE(token(payable), keyword(contract), + con(), tok('='), maybe_block(decl()), add_modifiers([_1], {contract_child, _2, _3, _5})) + , ?RULE(token(payable), keyword(contract), token(interface), + con(), tok('='), maybe_block(decl()), add_modifiers([_1], {contract_interface, _2, _4, _6})) + + , ?RULE(keyword(namespace), con(), tok('='), maybe_block(decl()), {namespace, _1, _2, _4}) , ?RULE(keyword(include), str(), {include, get_ann(_1), _2}) , pragma() diff --git a/src/aeso_pretty.erl b/src/aeso_pretty.erl index dfbd577..6b43b4c 100644 --- a/src/aeso_pretty.erl +++ b/src/aeso_pretty.erl @@ -13,6 +13,8 @@ -export_type([options/0]). +-include("aeso_utils.hrl"). + -type doc() :: prettypr:document(). -type options() :: [{indent, non_neg_integer()} | show_generated]. @@ -131,6 +133,10 @@ typed(A, Type) -> false -> follow(hsep(A, text(":")), type(Type)) end. +contract_head(contract_main) -> text("main contract"); +contract_head(contract_child) -> text("contract"); +contract_head(contract_interface) -> text("contract interface"). + %% -- Exports ---------------------------------------------------------------- -spec decls([aeso_syntax:decl()], options()) -> doc(). @@ -145,11 +151,11 @@ decl(D, Options) -> with_options(Options, fun() -> decl(D) end). -spec decl(aeso_syntax:decl()) -> doc(). -decl({contract, Attrs, C, Ds}) -> +decl({Con, Attrs, C, Ds}) when ?IS_CONTRACT_HEAD(Con) -> Mod = fun({Mod, true}) when Mod == payable -> text(atom_to_list(Mod)); (_) -> empty() end, - block(follow( hsep(lists:map(Mod, Attrs) ++ [text("contract")]) + block(follow( hsep(lists:map(Mod, Attrs) ++ [contract_head(Con)]) , hsep(name(C), text("="))), decls(Ds)); decl({namespace, _, C, Ds}) -> block(follow(text("namespace"), hsep(name(C), text("="))), decls(Ds)); diff --git a/src/aeso_scan.erl b/src/aeso_scan.erl index e81757f..2c5d301 100644 --- a/src/aeso_scan.erl +++ b/src/aeso_scan.erl @@ -44,7 +44,9 @@ lexer() -> , {"[^/*]+|[/*]", skip()} ], Keywords = ["contract", "include", "let", "switch", "type", "record", "datatype", "if", "elif", "else", "function", - "stateful", "payable", "true", "false", "mod", "public", "entrypoint", "private", "indexed", "namespace"], + "stateful", "payable", "true", "false", "mod", "public", "entrypoint", "private", "indexed", "namespace", + "interface", "main" + ], KW = string:join(Keywords, "|"), Rules = diff --git a/src/aeso_syntax.erl b/src/aeso_syntax.erl index 1d01ef0..cab8edc 100644 --- a/src/aeso_syntax.erl +++ b/src/aeso_syntax.erl @@ -25,7 +25,8 @@ -type ann_origin() :: system | user. -type ann_format() :: '?:' | hex | infix | prefix | elif. --type ann() :: [{line, ann_line()} | {col, ann_col()} | {format, ann_format()} | {origin, ann_origin()} | stateful | private]. +-type ann() :: [ {line, ann_line()} | {col, ann_col()} | {format, ann_format()} | {origin, ann_origin()} + | stateful | private | payable | main | interface]. -type name() :: string(). -type id() :: {id, ann(), name()}. @@ -34,7 +35,9 @@ -type qcon() :: {qcon, ann(), [name()]}. -type tvar() :: {tvar, ann(), name()}. --type decl() :: {contract, ann(), con(), [decl()]} +-type decl() :: {contract_main, ann(), con(), [decl()]} + | {contract_child, ann(), con(), [decl()]} + | {contract_interface, ann(), con(), [decl()]} | {namespace, ann(), con(), [decl()]} | {pragma, ann(), pragma()} | {type_decl, ann(), id(), [tvar()]} % Only for error msgs diff --git a/src/aeso_utils.hrl b/src/aeso_utils.hrl new file mode 100644 index 0000000..31fd2eb --- /dev/null +++ b/src/aeso_utils.hrl @@ -0,0 +1,6 @@ +-define(IS_CONTRACT_HEAD(X), + (X =:= contract_main orelse + X =:= contract_interface orelse + X =:= contract_child + ) + ). diff --git a/test/aeso_abi_tests.erl b/test/aeso_abi_tests.erl index 6f1fb02..9306657 100644 --- a/test/aeso_abi_tests.erl +++ b/test/aeso_abi_tests.erl @@ -190,7 +190,7 @@ parameterized_contract(ExtraCode, FunName, Types) -> lists:flatten( ["contract Remote =\n" " entrypoint bla : () => unit\n\n" - "contract Dummy =\n", + "main contract Dummy =\n", ExtraCode, "\n", " type an_alias('a) = string * 'a\n" " record r = {x : an_alias(int), y : variant}\n" diff --git a/test/aeso_aci_tests.erl b/test/aeso_aci_tests.erl index d570390..46db9e8 100644 --- a/test/aeso_aci_tests.erl +++ b/test/aeso_aci_tests.erl @@ -22,7 +22,8 @@ test_cases(1) -> MapACI = #{contract => #{name => <<"C">>, type_defs => [], - payable => true, + payable => true, + kind => contract_main, functions => [#{name => <<"a">>, arguments => @@ -31,56 +32,57 @@ test_cases(1) -> returns => <<"int">>, stateful => true, payable => true}]}}, - DecACI = <<"payable contract C =\n" + DecACI = <<"payable main contract C =\n" " payable entrypoint a : (int) => int\n">>, {Contract,MapACI,DecACI}; test_cases(2) -> - Contract = <<"contract C =\n" + Contract = <<"main contract C =\n" " type allan = int\n" " entrypoint a(i : allan) = i+1\n">>, MapACI = #{contract => - #{name => <<"C">>, payable => false, - type_defs => - [#{name => <<"allan">>, - typedef => <<"int">>, - vars => []}], - functions => - [#{arguments => - [#{name => <<"i">>, - type => <<"C.allan">>}], - name => <<"a">>, - returns => <<"int">>, - stateful => false, - payable => false}]}}, - DecACI = <<"contract C =\n" + #{name => <<"C">>, payable => false, + kind => contract_main, + type_defs => + [#{name => <<"allan">>, + typedef => <<"int">>, + vars => []}], + functions => + [#{arguments => + [#{name => <<"i">>, + type => <<"C.allan">>}], + name => <<"a">>, + returns => <<"int">>, + stateful => false, + payable => false}]}}, + DecACI = <<"main contract C =\n" " type allan = int\n" " entrypoint a : (C.allan) => int\n">>, {Contract,MapACI,DecACI}; test_cases(3) -> - Contract = <<"contract C =\n" + Contract = <<"main contract C =\n" " type state = unit\n" " datatype event = SingleEventDefined\n" - " datatype bert('a) = Bin('a)\n" - " entrypoint a(i : bert(string)) = 1\n">>, + " datatype bert('a) = Bin('a)\n" + " entrypoint a(i : bert(string)) = 1\n">>, MapACI = #{contract => #{functions => - [#{arguments => - [#{name => <<"i">>, - type => - #{<<"C.bert">> => [<<"string">>]}}], - name => <<"a">>,returns => <<"int">>, - stateful => false, payable => false}], - name => <<"C">>, payable => false, - event => #{variant => [#{<<"SingleEventDefined">> => []}]}, - state => <<"unit">>, + [#{arguments => + [#{name => <<"i">>, + type => + #{<<"C.bert">> => [<<"string">>]}}], + name => <<"a">>,returns => <<"int">>, + stateful => false, payable => false}], + name => <<"C">>, payable => false, kind => contract_main, + event => #{variant => [#{<<"SingleEventDefined">> => []}]}, + state => <<"unit">>, type_defs => - [#{name => <<"bert">>, - typedef => - #{variant => - [#{<<"Bin">> => [<<"'a">>]}]}, - vars => [#{name => <<"'a">>}]}]}}, - DecACI = <<"contract C =\n" + [#{name => <<"bert">>, + typedef => + #{variant => + [#{<<"Bin">> => [<<"'a">>]}]}, + vars => [#{name => <<"'a">>}]}]}}, + DecACI = <<"main contract C =\n" " type state = unit\n" " datatype event = SingleEventDefined\n" " datatype bert('a) = Bin('a)\n" @@ -101,17 +103,24 @@ aci_test_contract(Name) -> true -> [debug_mode]; false -> [] end ++ [{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), + JSON = case aeso_aci:contract_interface(json, String, Opts) of + {ok, J} -> J; + {error, ErrorStringJ} when is_binary(ErrorStringJ) -> error(ErrorStringJ); + {error, ErrorJ} -> aeso_compiler_tests:print_and_throw(ErrorJ) + end, + case aeso_compiler:from_string(String, [{aci, json}, {backend, fate} | Opts]) of + {ok, #{aci := JSON1}} -> + ?assertEqual(JSON, JSON1), + io:format("JSON:\n~p\n", [JSON]), + {ok, ContractStub} = aeso_aci:render_aci_json(JSON), - io:format("JSON:\n~p\n", [JSON]), - {ok, ContractStub} = aeso_aci:render_aci_json(JSON), + io:format("STUB:\n~s\n", [ContractStub]), + check_stub(ContractStub, [{src_file, Name}]), - io:format("STUB:\n~s\n", [ContractStub]), - check_stub(ContractStub, [{src_file, Name}]), - - ok. + ok; + {error, ErrorString} when is_binary(ErrorString) -> error(ErrorString); + {error, Error} -> aeso_compiler_tests:print_and_throw(Error) + end. check_stub(Stub, Options) -> try aeso_parser:string(binary_to_list(Stub), Options) of diff --git a/test/aeso_calldata_tests.erl b/test/aeso_calldata_tests.erl index c939893..58d66e3 100644 --- a/test/aeso_calldata_tests.erl +++ b/test/aeso_calldata_tests.erl @@ -59,8 +59,8 @@ calldata_aci_test_() -> end} || {ContractName, Fun, Args} <- compilable_contracts()]. parse_args(Fun, Args) -> - [{contract, _, _, [{letfun, _, _, _, _, {app, _, _, AST}}]}] = - aeso_parser:string("contract Temp = function foo() = " ++ Fun ++ "(" ++ string:join(Args, ", ") ++ ")"), + [{contract_main, _, _, [{letfun, _, _, _, _, {app, _, _, AST}}]}] = + aeso_parser:string("main contract Temp = function foo() = " ++ Fun ++ "(" ++ string:join(Args, ", ") ++ ")"), strip_ann(AST). strip_ann(T) when is_tuple(T) -> diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 36e70b4..00df6ac 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -34,9 +34,7 @@ simple_compile_test_() -> #{fate_code := Code} when Backend == fate -> Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), ?assertMatch({X, X}, {Code1, Code}); - ErrBin -> - io:format("\n~s", [ErrBin]), - error(ErrBin) + Error -> io:format("\n\n~p\n\n", [Error]), print_and_throw(Error) end end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], not lists:member(ContractName, not_compilable_on(Backend))] ++ @@ -179,17 +177,16 @@ compilable_contracts() -> "lhs_matching", "more_strings", "protected_call", - "hermetization_turnoff" + "hermetization_turnoff", + "multiple_contracts", + "clone", + "clone_simple", + "create", + "test" % Custom general-purpose test file. Keep it last on the list. ]. 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" - , "hermetization_turnoff" - - ]. +not_compilable_on(aevm) -> compilable_contracts(). debug_mode_contracts() -> ["hermetization_turnoff"]. @@ -635,9 +632,9 @@ failing_contracts() -> <>]) - , ?TYPE_ERROR(multiple_contracts, + , ?TYPE_ERROR(interface_with_defs, [<>]) , ?TYPE_ERROR(contract_as_namespace, [< , ?TYPE_ERROR(bad_state, [<>]) + , ?TYPE_ERROR(factories_type_errors, + [<>, + < if(protected, option(void), void)\n and (gas : int, value : int, protected : bool, int, bool) => 'b\n" + "when checking contract construction of type\n (gas : int, value : int, protected : bool) =>\n if(protected, option(void), void) (at line 11, column 18)\nagainst the expected type\n (gas : int, value : int, protected : bool, int, bool) => 'b">>, + <>, + <>, + < if(protected, option(void), void)\n and (gas : int, value : int, protected : bool) => 'a\n" + "when checking contract construction of type\n (gas : int, value : int, protected : bool, int, bool) =>\n if(protected, option(void), void) (at line 18, column 18)\nagainst the expected type\n (gas : int, value : int, protected : bool) => 'a">>, + <>, + <> + ]) + , ?TYPE_ERROR(ambiguous_main, + [<> + ]) + , ?TYPE_ERROR(no_main_contract, + [<> + ]) + , ?TYPE_ERROR(multiple_main_contracts, + [<> + ]) ]. -define(Path(File), "code_errors/" ??File). @@ -746,9 +776,7 @@ failing_contracts() -> {fate, ?Msg(File, Line, Col, ErrFATE)}]}). failing_code_gen_contracts() -> - [ ?SAME(last_declaration_must_be_contract, 1, 1, - "Expected a contract as the last declaration instead of the namespace 'LastDeclarationIsNotAContract'") - , ?SAME(missing_definition, 2, 14, + [ ?SAME(missing_definition, 2, 14, "Missing definition of function 'foo'.") , ?AEVM(polymorphic_entrypoint, 2, 17, "The argument\n" @@ -846,6 +874,8 @@ failing_code_gen_contracts() -> "Invalid state type\n" " {f : (int) => int}\n" "The state cannot contain functions in the AEVM. Use FATE if you need this.") + , ?FATE(child_with_decls, 2, 14, + "Missing definition of function 'f'.") ]. validation_test_() -> @@ -883,14 +913,26 @@ validation_fails() -> "Byte code contract is not payable, but source code contract is.">>]}]. validate(Contract1, Contract2) -> - ByteCode = #{ fate_code := FCode } = compile(fate, Contract1), - FCode1 = aeb_fate_code:serialize(aeb_fate_code:strip_init_function(FCode)), - Source = aeso_test_utils:read_contract(Contract2), - aeso_compiler:validate_byte_code( - ByteCode#{ byte_code := FCode1 }, Source, - case lists:member(Contract2, debug_mode_contracts()) of - true -> [debug_mode]; - false -> [] - end ++ - [{backend, fate}, {include, {file_system, [aeso_test_utils:contract_path()]}}]). + case compile(fate, Contract1) of + ByteCode = #{ fate_code := FCode } -> + FCode1 = aeb_fate_code:serialize(aeb_fate_code:strip_init_function(FCode)), + Source = aeso_test_utils:read_contract(Contract2), + aeso_compiler:validate_byte_code( + ByteCode#{ byte_code := FCode1 }, Source, + case lists:member(Contract2, debug_mode_contracts()) of + true -> [debug_mode]; + false -> [] + end ++ + [{backend, fate}, {include, {file_system, [aeso_test_utils:contract_path()]}}]); + Error -> print_and_throw(Error) + end. +print_and_throw(Err) -> + case Err of + ErrBin when is_binary(ErrBin) -> + io:format("\n~s", [ErrBin]), + error(ErrBin); + Errors -> + io:format("Compilation error:\n~s", [string:join([aeso_errors:pp(E) || E <- Errors], "\n\n")]), + error(compilation_error) + end. diff --git a/test/aeso_parser_tests.erl b/test/aeso_parser_tests.erl index c978b0a..f8e2eee 100644 --- a/test/aeso_parser_tests.erl +++ b/test/aeso_parser_tests.erl @@ -12,10 +12,10 @@ simple_contracts_test_() -> fun(_) -> ok end, [{"Parse a contract with an identity function.", fun() -> - Text = "contract Identity =\n" + Text = "main contract Identity =\n" " function id(x) = x\n", ?assertMatch( - [{contract, _, {con, _, "Identity"}, + [{contract_main, _, {con, _, "Identity"}, [{letfun, _, {id, _, "id"}, [{id, _, "x"}], {id, _, "_"}, {id, _, "x"}}]}], parse_string(Text)), ok diff --git a/test/contracts/__call.aes b/test/contracts/__call.aes index 1e19b5b..1f90e04 100644 --- a/test/contracts/__call.aes +++ b/test/contracts/__call.aes @@ -1,5 +1,5 @@ contract Identity = - function main (x:int) = x + function main_fun (x:int) = x function __call() = 12 diff --git a/test/contracts/address_chain.aes b/test/contracts/address_chain.aes index 4c55a45..da5a111 100644 --- a/test/contracts/address_chain.aes +++ b/test/contracts/address_chain.aes @@ -1,5 +1,5 @@ -contract Remote = - entrypoint main : (int) => unit +contract interface Remote = + entrypoint main_fun : (int) => unit contract AddrChain = type o_type = oracle(string, map(string, int)) diff --git a/test/contracts/address_literals.aes b/test/contracts/address_literals.aes index 15007cf..94e7e0c 100644 --- a/test/contracts/address_literals.aes +++ b/test/contracts/address_literals.aes @@ -1,5 +1,5 @@ -contract Remote = +contract interface Remote = entrypoint foo : () => unit contract AddressLiterals = diff --git a/test/contracts/ambiguous_main.aes b/test/contracts/ambiguous_main.aes new file mode 100644 index 0000000..d375fa9 --- /dev/null +++ b/test/contracts/ambiguous_main.aes @@ -0,0 +1,5 @@ +contract C = + entrypoint f() = 123 + +contract D = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/bad_address_literals.aes b/test/contracts/bad_address_literals.aes index 1750846..30eebac 100644 --- a/test/contracts/bad_address_literals.aes +++ b/test/contracts/bad_address_literals.aes @@ -1,5 +1,5 @@ -contract Remote = +contract interface Remote = entrypoint foo : () => unit contract AddressLiterals = diff --git a/test/contracts/bad_protected_call.aes b/test/contracts/bad_protected_call.aes index ba1f59c..9b7cf9d 100644 --- a/test/contracts/bad_protected_call.aes +++ b/test/contracts/bad_protected_call.aes @@ -1,4 +1,4 @@ -contract Remote = +contract interface Remote = entrypoint id : int => int contract ProtectedCall = diff --git a/test/contracts/bad_top_level_decl.aes b/test/contracts/bad_top_level_decl.aes index 5475fb6..e9053aa 100644 --- a/test/contracts/bad_top_level_decl.aes +++ b/test/contracts/bad_top_level_decl.aes @@ -1,3 +1,3 @@ function square(x) = x ^ 2 contract Main = - entrypoint main() = square(10) + entrypoint main_fun() = square(10) diff --git a/test/contracts/clone.aes b/test/contracts/clone.aes new file mode 100644 index 0000000..26c41d4 --- /dev/null +++ b/test/contracts/clone.aes @@ -0,0 +1,28 @@ + +contract interface HigherOrderState = + entrypoint init : () => void + entrypoint apply : int => int + stateful entrypoint inc : int => unit + +contract interface LowerDisorderAnarchy = + entrypoint init : (int) => void + + +main contract C = + // both `s` and `l` should be of type `HigherOrderState` in this test + stateful entrypoint run_clone(s : HigherOrderState, l : LowerDisorderAnarchy) : HigherOrderState = + let s1 = Chain.clone(ref=s) + let Some(s2) = Chain.clone(ref=s, protected=true) + let None = Chain.clone(ref=s, protected=true, gas=1) + let None = Chain.clone(ref=l, protected=true, 123) // since it should be HigherOrderState underneath + let s3 = Chain.clone(ref=s1) + require(s1.apply(2137) == 2137, "APPLY_S1_0") + require(s2.apply(2137) == 2137, "APPLY_S2_0") + require(s3.apply(2137) == 2137, "APPLY_S3_0") + s1.inc(1) + s2.inc(1) + s1.inc(1) + require(s1.apply(2137) == 2139, "APPLY_S1_2") + require(s2.apply(2137) == 2138, "APPLY_S2_1") + require(s3.apply(2137) == 2137, "APPLY_S3_0") + s1 \ No newline at end of file diff --git a/test/contracts/clone_simple.aes b/test/contracts/clone_simple.aes new file mode 100644 index 0000000..f51fa64 --- /dev/null +++ b/test/contracts/clone_simple.aes @@ -0,0 +1,7 @@ +contract interface I = + entrypoint init : () => void + +contract C = + stateful entrypoint f(i : I) = + let Some(c1) = Chain.clone(ref=i, protected = true) + 2 \ No newline at end of file diff --git a/test/contracts/code_errors/bad_aens_resolve.aes b/test/contracts/code_errors/bad_aens_resolve.aes index 38d932b..8700dfb 100644 --- a/test/contracts/code_errors/bad_aens_resolve.aes +++ b/test/contracts/code_errors/bad_aens_resolve.aes @@ -5,5 +5,5 @@ contract BadAENSresolve = function fail() : t(int) = AENS.resolve("foo.aet", "whatever") - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/child_with_decls.aes b/test/contracts/code_errors/child_with_decls.aes new file mode 100644 index 0000000..4b2d469 --- /dev/null +++ b/test/contracts/code_errors/child_with_decls.aes @@ -0,0 +1,5 @@ +contract C = + entrypoint f : () => unit + +main contract M = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/code_errors/higher_order_map_keys.aes b/test/contracts/code_errors/higher_order_map_keys.aes index a5fa742..98327e2 100644 --- a/test/contracts/code_errors/higher_order_map_keys.aes +++ b/test/contracts/code_errors/higher_order_map_keys.aes @@ -3,4 +3,4 @@ contract MapAsMapKey = function foo(m) : t(int => int) = {[m] = 0} - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/higher_order_query_type.aes b/test/contracts/code_errors/higher_order_query_type.aes index 39d533f..1f4a4e6 100644 --- a/test/contracts/code_errors/higher_order_query_type.aes +++ b/test/contracts/code_errors/higher_order_query_type.aes @@ -2,4 +2,4 @@ contract HigherOrderQueryType = stateful function foo(o) : oracle_query(_, string ) = Oracle.query(o, (x) => x + 1, 100, RelativeTTL(100), RelativeTTL(100)) - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/higher_order_response_type.aes b/test/contracts/code_errors/higher_order_response_type.aes index ae4ec93..87da03c 100644 --- a/test/contracts/code_errors/higher_order_response_type.aes +++ b/test/contracts/code_errors/higher_order_response_type.aes @@ -2,4 +2,4 @@ contract HigherOrderResponseType = stateful function foo(o, q : oracle_query(string, _)) = Oracle.respond(o, q, (x) => x + 1) - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/last_declaration_must_be_contract.aes b/test/contracts/code_errors/last_declaration_must_be_contract.aes deleted file mode 100644 index 1c72d81..0000000 --- a/test/contracts/code_errors/last_declaration_must_be_contract.aes +++ /dev/null @@ -1,2 +0,0 @@ -namespace LastDeclarationIsNotAContract = - function add(x, y) = x + y diff --git a/test/contracts/code_errors/missing_definition.aes b/test/contracts/code_errors/missing_definition.aes index 6f7b919..5e354ec 100644 --- a/test/contracts/code_errors/missing_definition.aes +++ b/test/contracts/code_errors/missing_definition.aes @@ -1,3 +1,3 @@ contract MissingDefinition = entrypoint foo : int => int - entrypoint main() = foo(0) + entrypoint main_fun() = foo(0) diff --git a/test/contracts/code_errors/polymorphic_aens_resolve.aes b/test/contracts/code_errors/polymorphic_aens_resolve.aes index 6301743..0bd9cc8 100644 --- a/test/contracts/code_errors/polymorphic_aens_resolve.aes +++ b/test/contracts/code_errors/polymorphic_aens_resolve.aes @@ -3,5 +3,5 @@ contract PolymorphicAENSresolve = function fail() : option('a) = AENS.resolve("foo.aet", "whatever") - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/polymorphic_map_keys.aes b/test/contracts/code_errors/polymorphic_map_keys.aes index eb64237..9821780 100644 --- a/test/contracts/code_errors/polymorphic_map_keys.aes +++ b/test/contracts/code_errors/polymorphic_map_keys.aes @@ -3,4 +3,4 @@ contract MapAsMapKey = function foo(m) : t('a) = {[m] = 0} - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/polymorphic_query_type.aes b/test/contracts/code_errors/polymorphic_query_type.aes index 4fba99b..0f5d6ab 100644 --- a/test/contracts/code_errors/polymorphic_query_type.aes +++ b/test/contracts/code_errors/polymorphic_query_type.aes @@ -2,4 +2,4 @@ contract PolymorphicQueryType = stateful function is_oracle(o) = Oracle.check(o) - entrypoint main() = () + entrypoint main_fun() = () diff --git a/test/contracts/code_errors/polymorphic_response_type.aes b/test/contracts/code_errors/polymorphic_response_type.aes index 9b33971..3c26af5 100644 --- a/test/contracts/code_errors/polymorphic_response_type.aes +++ b/test/contracts/code_errors/polymorphic_response_type.aes @@ -2,4 +2,4 @@ contract PolymorphicResponseType = function is_oracle(o : oracle(string, 'r)) = Oracle.check(o) - entrypoint main(o : oracle(string, int)) = is_oracle(o) + entrypoint main_fun(o : oracle(string, int)) = is_oracle(o) diff --git a/test/contracts/code_errors/unapplied_contract_call.aes b/test/contracts/code_errors/unapplied_contract_call.aes index 0ef0248..78f5a12 100644 --- a/test/contracts/code_errors/unapplied_contract_call.aes +++ b/test/contracts/code_errors/unapplied_contract_call.aes @@ -1,4 +1,4 @@ -contract Remote = +contract interface Remote = entrypoint foo : int => int contract UnappliedContractCall = diff --git a/test/contracts/code_errors/unapplied_named_arg_builtin.aes b/test/contracts/code_errors/unapplied_named_arg_builtin.aes index 9b0a5ce..ebd2d1a 100644 --- a/test/contracts/code_errors/unapplied_named_arg_builtin.aes +++ b/test/contracts/code_errors/unapplied_named_arg_builtin.aes @@ -1,5 +1,5 @@ contract UnappliedNamedArgBuiltin = // Allowed in FATE, but not AEVM - stateful entrypoint main(s) = + stateful entrypoint main_fun(s) = let reg = Oracle.register reg(signature = s, Contract.address, 100, RelativeTTL(100)) : oracle(int, int) diff --git a/test/contracts/complex_types.aes b/test/contracts/complex_types.aes index 57e19d7..83b9b69 100644 --- a/test/contracts/complex_types.aes +++ b/test/contracts/complex_types.aes @@ -1,5 +1,5 @@ -contract Remote = +contract interface Remote = entrypoint up_to : (int) => list(int) entrypoint sum : (list(int)) => int entrypoint some_string : () => string diff --git a/test/contracts/contract_as_namespace.aes b/test/contracts/contract_as_namespace.aes index 489dfda..ac4de29 100644 --- a/test/contracts/contract_as_namespace.aes +++ b/test/contracts/contract_as_namespace.aes @@ -1,4 +1,4 @@ -contract Foo = +contract interface Foo = entrypoint foo : () => int contract Fail = diff --git a/test/contracts/create.aes b/test/contracts/create.aes new file mode 100644 index 0000000..5886b20 --- /dev/null +++ b/test/contracts/create.aes @@ -0,0 +1,28 @@ +contract IntegerAdder = + entrypoint init() = () + entrypoint addIntegers(x, y) = x + y + +contract IntegerAdderHolder = + type state = IntegerAdder + stateful entrypoint init() = Chain.create() : IntegerAdder + entrypoint get() = state + +contract IntegerAdderFactory = + entrypoint init() = () + stateful entrypoint new() = + let i = Chain.create() : IntegerAdderHolder + i.get() + +payable contract ValueAdder = + entrypoint init() = () + stateful entrypoint addValue(x) = + let integerAdderFactory = Chain.create() + let adder = integerAdderFactory.new() + adder.addIntegers(x, Contract.balance) + +main contract EnterpriseContract = + entrypoint init() = () + stateful payable entrypoint increaseByThree(x) = + require(Call.value >= 3, "Price for addition = 3AEtto, insufficient funds") + let threeAdder = Chain.create(value = 3) + threeAdder.addValue(x) diff --git a/test/contracts/environment.aes b/test/contracts/environment.aes index d02eec5..a05f5bf 100644 --- a/test/contracts/environment.aes +++ b/test/contracts/environment.aes @@ -1,6 +1,6 @@ // Testing primitives for accessing the block chain environment -contract Interface = +contract interface Interface = entrypoint contract_address : () => address entrypoint call_origin : () => address entrypoint call_caller : () => address diff --git a/test/contracts/events.aes b/test/contracts/events.aes index 74a3393..b63397c 100644 --- a/test/contracts/events.aes +++ b/test/contracts/events.aes @@ -1,4 +1,4 @@ -contract Remote = +contract interface Remote = entrypoint dummy : () => unit contract Events = diff --git a/test/contracts/factorial.aes b/test/contracts/factorial.aes index 88a8869..800ef07 100644 --- a/test/contracts/factorial.aes +++ b/test/contracts/factorial.aes @@ -1,6 +1,6 @@ // An implementation of the factorial function where each recursive // call is to another contract. Not the cheapest way to compute factorial. -contract FactorialServer = +contract interface FactorialServer = entrypoint fac : (int) => int contract Factorial = diff --git a/test/contracts/factories_type_errors.aes b/test/contracts/factories_type_errors.aes new file mode 100644 index 0000000..2a5ea00 --- /dev/null +++ b/test/contracts/factories_type_errors.aes @@ -0,0 +1,24 @@ +contract interface Kaboom = + entrypoint init : () => void + +contract Bakoom = + type state = int + entrypoint init(x, b) = if(b) x else 0 + +main contract Test = + stateful entrypoint test(k : Kaboom) = + let k_bad1 = Chain.clone() : Kaboom + let k_bad2 = Chain.clone(ref=k, 123, true) + let k_bad3 = Chain.clone(ref=k, gas=true) + let k_bad4 = Chain.create() : Kaboom + let k_gud1 = Chain.clone(ref=k) + let Some(k_gud2) = Chain.clone(ref=k, protected=true) + let Some(k_gud3) = Chain.clone(ref=k, value=10, protected=true, gas=123) + + let b_bad1 = Chain.create() : Bakoom + let b_bad2 = Chain.create(123, true, protected=true) : Bakoom + let b_bad3 = Chain.create(123, true, value=true) : Bakoom + let b_gud1 = Chain.create(123, true) : Bakoom + let b_gud2 = Chain.create(123, true, value=100) : Bakoom + + b_gud1 \ No newline at end of file diff --git a/test/contracts/identity.aes b/test/contracts/identity.aes index cc5eeaf..2a639f6 100644 --- a/test/contracts/identity.aes +++ b/test/contracts/identity.aes @@ -1,3 +1,2 @@ - -contract Identity = - entrypoint main (x:int) = x +main contract Identity = + entrypoint main_fun (x:int) = x diff --git a/test/contracts/interface_with_defs.aes b/test/contracts/interface_with_defs.aes new file mode 100644 index 0000000..8f91843 --- /dev/null +++ b/test/contracts/interface_with_defs.aes @@ -0,0 +1,5 @@ +contract interface ContractOne = + entrypoint foo() = "foo" + +contract ContractTwo = + entrypoint bar() = "bar" diff --git a/test/contracts/lhs_matching.aes b/test/contracts/lhs_matching.aes index 2cafa9d..98dddbb 100644 --- a/test/contracts/lhs_matching.aes +++ b/test/contracts/lhs_matching.aes @@ -16,7 +16,7 @@ contract LHSMatching = let null(_ :: _) = false !null(xs) - entrypoint main() = + entrypoint main_fun() = from_some(Some([0])) ++ append([length([true]), 2, 3], [4, 5, 6]) ++ [7 | if (local_match([false]))] diff --git a/test/contracts/missing_event_type.aes b/test/contracts/missing_event_type.aes index 8931c5e..745fcf4 100644 --- a/test/contracts/missing_event_type.aes +++ b/test/contracts/missing_event_type.aes @@ -1,3 +1,3 @@ contract MissingEventType = - entrypoint main() = + entrypoint main_fun() = Chain.event("MAIN") diff --git a/test/contracts/multiple_contracts.aes b/test/contracts/multiple_contracts.aes index 41df9b4..5d71b36 100644 --- a/test/contracts/multiple_contracts.aes +++ b/test/contracts/multiple_contracts.aes @@ -1,5 +1,7 @@ -contract ContractOne = - entrypoint foo() = "foo" +contract Child = + entrypoint + add2 : int => int + add2(x) = x + 2 -contract ContractTwo = - entrypoint bar() = "bar" +main contract Main = + entrypoint add4(x, c : Child) = c.add2(x) + 2 \ No newline at end of file diff --git a/test/contracts/multiple_main_contracts.aes b/test/contracts/multiple_main_contracts.aes new file mode 100644 index 0000000..6c85bb1 --- /dev/null +++ b/test/contracts/multiple_main_contracts.aes @@ -0,0 +1,5 @@ +main contract C = + entrypoint f() = 123 + +main contract D = + entrypoint f() = 123 \ No newline at end of file diff --git a/test/contracts/no_main_contract.aes b/test/contracts/no_main_contract.aes new file mode 100644 index 0000000..4b0e9b4 --- /dev/null +++ b/test/contracts/no_main_contract.aes @@ -0,0 +1,2 @@ +contract interface C = + entrypoint f : () => unit \ No newline at end of file diff --git a/test/contracts/non_functional_entrypoint.aes b/test/contracts/non_functional_entrypoint.aes index 4162e38..0b23b5a 100644 --- a/test/contracts/non_functional_entrypoint.aes +++ b/test/contracts/non_functional_entrypoint.aes @@ -1,4 +1,4 @@ -contract C1 = +contract interface C1 = entrypoint f : int contract C = diff --git a/test/contracts/protected_call.aes b/test/contracts/protected_call.aes index d377399..6bf09b2 100644 --- a/test/contracts/protected_call.aes +++ b/test/contracts/protected_call.aes @@ -1,4 +1,4 @@ -contract Remote = +contract interface Remote = entrypoint id : int => int contract ProtectedCall = diff --git a/test/contracts/remote_call.aes b/test/contracts/remote_call.aes index e644a0f..c5b0547 100644 --- a/test/contracts/remote_call.aes +++ b/test/contracts/remote_call.aes @@ -1,18 +1,18 @@ -contract Remote1 = - entrypoint main : (int) => int +contract interface Remote1 = + entrypoint main_fun : (int) => int -contract Remote2 = +contract interface Remote2 = entrypoint call : (Remote1, int) => int -contract Remote3 = +contract interface Remote3 = entrypoint get : () => int entrypoint tick : () => unit contract RemoteCall = stateful entrypoint call(r : Remote1, x : int) : int = - r.main(gas = 10000, value = 10, x) + r.main_fun(gas = 10000, value = 10, x) entrypoint staged_call(r1 : Remote1, r2 : Remote2, x : int) = r2.call(r1, x) diff --git a/test/contracts/spend_test.aes b/test/contracts/spend_test.aes index 7213959..0eb1666 100644 --- a/test/contracts/spend_test.aes +++ b/test/contracts/spend_test.aes @@ -1,5 +1,5 @@ -contract SpendContract = +contract interface SpendContract = entrypoint withdraw : (int) => int contract SpendTest = diff --git a/test/contracts/state_handling.aes b/test/contracts/state_handling.aes index 00c2fcb..67722ba 100644 --- a/test/contracts/state_handling.aes +++ b/test/contracts/state_handling.aes @@ -1,5 +1,5 @@ include "String.aes" -contract Remote = +contract interface Remote = record rstate = { i : int, s : string, m : map(int, int) } entrypoint look_at : (rstate) => unit diff --git a/test/contracts/stateful.aes b/test/contracts/stateful.aes index e6ea8be..7237b6e 100644 --- a/test/contracts/stateful.aes +++ b/test/contracts/stateful.aes @@ -1,5 +1,5 @@ -contract Remote = +contract interface Remote = stateful entrypoint remote_spend : (address, int) => unit entrypoint remote_pure : int => int diff --git a/test/contracts/test.aes b/test/contracts/test.aes index bd1a130..2b3783e 100644 --- a/test/contracts/test.aes +++ b/test/contracts/test.aes @@ -1,102 +1,12 @@ +// This is a custom test file if you need to run a compiler without +// changing aeso_compiler_tests.erl -contract Identity = - // type xy = {x:int, y:int} - // type xz = {x:int, z:int} - // type yz = {y:int, z:int} - record point = {x:int,y:int} - record cp('a) = {color: string, p:'a} - //type intpoint = point(int) - // //if (x==42) 1 else (x*x) - // } - //let baz() = {age:3, name:(4:int)} - //let foo(a,b,c) = c - // let rec fac(n) = if((n:int)==0) 1 else (n*fac(n-1)) - // and main(x) = x::[x+1] - // let lentr(l) = lent(0,l) - // let rec len(l) = - // switch(l) { - // | [] => 0 - // | x::xs => 1+len(xs) - // } - // let lent(n,l) = - // switch (l) { - // | [] => n - // | (x::xs) => lent(n+1,xs) - // } - // let rec app(a,b) = - // switch(a) { - // | [] => b - // | (x::xs) => x::app(xs,b) - // } - // let rec revt(l,r) = - // switch(l) { - // | [] => r - // | x::xs => revt(xs,x::r) - // } - // let rev(l) = revt(l,[]) - // let main(x:int) = { - // switch(rev([1,2,3])) { - // | h::_ => h - // } - // } - //let fac(n:int) = { - // if (n==0) 1 else (n*fac(n-1)) - //} - //let main(x) = switch((12,34)) { - //| (13,_) => x - //| (_,a) => x+a - // | y => y+1 - // } - //let main(x) = ({y:0>1, x:x==0}:point(bool)) - //let main(x) = x - //let main(x) = len(1::2::[]) - //let main(x) = ((x,x):list('a)) - // let main(x) = switch("a") { - // | "b" => 0 - // | "a" => 1 - // | "c" => 2 - // } - //let main(x) = x.color+1 - //let main(x) = switch(({x:x, y:x+1}:cp(int))) { - // | {y:xx} => xx - // } - //let main(x) = {x:0, y:1, z:2} - // let id(x) = x - // let double(x) = x+x - // let pair(x) = (1,2) - // let unit(x) = () - // let tuples(x) = ((1,x),(2,3,4)) - // let singleton(x) = [x] - // let rec seq(n) = if (n==0) [] else (app(seq(n-1),[n])) - // let idString(s:string) = s - // let pairString(s:string) = (s,s) - // let revStrings(ss:list(string))=rev(ss) - // let makePoint(x,y) = {x:x, y:y} - // let getx(x) = x.x - // let updatex(p,x) = p{x:x} - // let quad(x) = {let y=x+x; let z=y+y; z;} - // let noblock(x) = {x; x} - // let unit(x) = () - // let foo(x) = switch (x) { - // | y => y+1 -// } - // let p(x) = {color:"blue", p:{x:x, y:x+1}} - //let twice(f,x) = f(f(x)) - // let twice(f,x) = f(f(x)) - // let double(x) = x+x - // let main(x) = twice((y=>y+y),x) - // let rec map(f,xs) = switch(xs) { - // | [] => [] - // | (x::ys) => f(x)::map(f,ys) - // } - // let id(x) = x - // let main(xs) = map(double,xs) - function z(f,x) = x - function s(n) = (f,x)=>f(n(f,x)) - function add(m,n) = (f,x)=>m(f,n(f,x)) +include "List.aes" - entrypoint main() = - let three=s(s(s(z))) - add(three,three) - (((i)=>i+1),0) +contract IntegerHolder = + type state = int + entrypoint init(x) = x + entrypoint get() = state +main contract Test = + stateful entrypoint f(c) = Chain.clone(ref=c, 123) \ No newline at end of file diff --git a/test/contracts/type_clash.aes b/test/contracts/type_clash.aes index dd20607..fb95e4c 100644 --- a/test/contracts/type_clash.aes +++ b/test/contracts/type_clash.aes @@ -1,5 +1,5 @@ -contract Remote = +contract interface Remote = type themap = map(int, string) entrypoint foo : () => themap diff --git a/test/contracts/unapplied_builtins.aes b/test/contracts/unapplied_builtins.aes index 678b085..1f2e9ae 100644 --- a/test/contracts/unapplied_builtins.aes +++ b/test/contracts/unapplied_builtins.aes @@ -9,7 +9,7 @@ // Oracle.extend include "String.aes" contract UnappliedBuiltins = - entrypoint main() = () + entrypoint main_fun() = () type o = oracle(int, int) type t = list(int * string) type m = map(int, int)