From 262452fb7087a9266cb77b08bfac664dc333a445 Mon Sep 17 00:00:00 2001 From: Marco Walz <8124114+marc0olo@users.noreply.github.com> Date: Fri, 27 Aug 2021 16:46:18 +0200 Subject: [PATCH] Feature/mkdocs with versioning (#333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: restructuring & introduction of mkdocs with versioning provided by mike * docs: ad repositories section to sophia examples * docs: refactoring and consistent naming of æternity * docs: hint for new file destination * docs: revert capital letter * docs: accept proposed changes * docs: fix anchors in stdlib --- .docssite/docs/favicon.png | Bin 0 -> 1045 bytes .docssite/hook.py | 7 + .docssite/mkdocs.yml | 55 ++ .docssite/overrides/main.html | 8 + .github/workflows/docs-develop.yml | 25 + .github/workflows/docs-release.yml | 26 + .github/workflows/requirements.txt | 4 + .gitignore | 2 + LICENSE | 2 +- README.md | 16 +- docs/index.md | 12 + docs/sophia.md | 1196 +--------------------------- docs/sophia_examples.md | 73 ++ docs/sophia_features.md | 783 ++++++++++++++++++ docs/sophia_stdlib.md | 298 +++---- docs/sophia_syntax.md | 263 ++++++ 16 files changed, 1419 insertions(+), 1351 deletions(-) create mode 100644 .docssite/docs/favicon.png create mode 100644 .docssite/hook.py create mode 100644 .docssite/mkdocs.yml create mode 100644 .docssite/overrides/main.html create mode 100644 .github/workflows/docs-develop.yml create mode 100644 .github/workflows/docs-release.yml create mode 100644 .github/workflows/requirements.txt create mode 100644 docs/index.md create mode 100644 docs/sophia_examples.md create mode 100644 docs/sophia_features.md create mode 100644 docs/sophia_syntax.md diff --git a/.docssite/docs/favicon.png b/.docssite/docs/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e81502d439e5fd8e4d9795a9ac2daada69c71053 GIT binary patch literal 1045 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFnVW(M3hAM`dB6B=jtV<?;Zqle1Gx6p~WYGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&% zT$P<{nWAKG$7NGt1vDTxwIorYA~z?m*s8)-32d$vkPQ;nS5g2gDap1~as*kZ5aAo3 z;GAESs$i;TrkiYNY@uLosb^?vX<=ldqhMrUXr^yqsc&eaYhY+)U}!BPbD zzk=sFD+UIpR!6#*-mlngTv_|`r=ZTEqB93H?i~DX(Yg4?+W9jMf0UZ` zFp}q>X-)IvA0oE3f)!l9H!7UJyQAy4fn2FY|Mr4yPM;zz8Zs;QHAzb}FWXYvxb+7k zSLU1+f1{s~dwM!6_<}#!^gZeIdwkVylK!#unSWH$9?k4A6y(0xS($P8N)W4*ab@wQ z{G;&fB&mJ92$>YRNddm~2Ga!7^Z^lO7EeXncA%%JmW#mMY<|?#&<*pNvdiD1B|M!3XbD4;^ z_OIif$|rKWRWEANk9jI#eHRz`_WoFXrcPXW@{1)(ZJp^wuNgV+9J2nwW&NR4URDIl`J$I2*{gl6Tu3vuN7gZ<;YnOJdX%GLx&W@Si(9l?VI^LEau$#QsxixJZ>GFe@AIX|O|0z}H_ncF5h4-7Lb6n + Click here to go to latest. + +{% endblock %} \ No newline at end of file diff --git a/.github/workflows/docs-develop.yml b/.github/workflows/docs-develop.yml new file mode 100644 index 0000000..48bc7f6 --- /dev/null +++ b/.github/workflows/docs-develop.yml @@ -0,0 +1,25 @@ +name: Publish development docs +on: + push: + branches: ['master'] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - uses: actions/cache@v2 + with: + path: ~/.cache/pip3 + key: ${{ runner.os }}-pip-${{ hashFiles('.github/workflows/requirements.txt') }} + - run: pip3 install -r .github/workflows/requirements.txt + - run: git config --global user.email "github-action@users.noreply.github.com" + - run: git config --global user.name "GitHub Action" + - run: | + cd .docssite + mike deploy --push master \ No newline at end of file diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml new file mode 100644 index 0000000..4ec58e2 --- /dev/null +++ b/.github/workflows/docs-release.yml @@ -0,0 +1,26 @@ +name: Publish release docs +on: + release: + types: [released] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - uses: actions/cache@v2 + with: + path: ~/.cache/pip3 + key: ${{ runner.os }}-pip-${{ hashFiles('.github/workflows/requirements.txt') }} + - run: pip3 install -r .github/workflows/requirements.txt + - run: git config --global user.email "github-action@users.noreply.github.com" + - run: git config --global user.name "GitHub Action" + - run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV + - run: | + cd .docssite + mike deploy --push --update-aliases $RELEASE_VERSION latest \ No newline at end of file diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt new file mode 100644 index 0000000..61d5b2b --- /dev/null +++ b/.github/workflows/requirements.txt @@ -0,0 +1,4 @@ +mkdocs==1.2.1 +mkdocs-simple-hooks==0.1.3 +mkdocs-material==7.1.9 +mike==1.0.1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index f7ebb14..095c8b0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ aesophia .qcci current_counterexample.eqc test/contracts/test.aes +__pycache__ +.docssite/docs/*.md diff --git a/LICENSE b/LICENSE index 142825a..b4cc822 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2017, aeternity developers +Copyright (c) 2017, æternity developers Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.md b/README.md index 68bcb6d..e11896a 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,17 @@ This is the __sophia__ compiler for the æternity system which compiles contract The compiler is currently being used three places - [The command line compiler](https://github.com/aeternity/aesophia_cli) - [The HTTP compiler](https://github.com/aeternity/aesophia_http) - - In [Aeternity node](https://github.com/aeternity/aeternity) tests + - In [æternity node](https://github.com/aeternity/aeternity) tests ## Documentation -* [Smart Contracts on aeternity Blockchain](https://github.com/aeternity/protocol/blob/master/contracts/contracts.md). -* [Sophia Documentation](docs/sophia.md). -* [Sophia Standard Library](docs/sophia_stdlib.md). +* [Introduction](docs/index.md) +* [Syntax](docs/sophia_syntax.md) +* [Features](docs/sophia_features.md) +* [Standard library](docs/sophia_stdlib.md) +* [Contract examples](docs/sophia_examples.md) + +Additionally you can check out the [contracts section](https://github.com/aeternity/protocol/blob/master/contracts/contracts.md) of the æternity blockchain specification. ## Versioning @@ -26,5 +30,5 @@ Versioning should follow the [semantic versioning](https://semver.org/spec/v2.0. The basic modules for interfacing the compiler: -* [aeso_compiler: the Sophia compiler](./docs/aeso_compiler.md) -* [aeso_aci: the ACI interface](./docs/aeso_aci.md) +* [aeso_compiler: the Sophia compiler](docs/aeso_compiler.md) +* [aeso_aci: the ACI interface](docs/aeso_aci.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b3f2129 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# Introduction +Sophia is a functional language designed for smart contract development. It is strongly typed and has +restricted mutable state. + +Sophia is customized for smart contracts, which can be published +to a blockchain. Thus some features of conventional +languages, such as floating point arithmetic, are not present in Sophia, and +some [æternity blockchain](https://aeternity.com) specific primitives, constructions and types have been added. + +!!! Note + - For rapid prototyping of smart contracts check out [AEstudio](https://studio.aepps.com/)! + - For playing around and diving deeper into the language itself check out the [REPL](https://repl.aeternity.io/)! diff --git a/docs/sophia.md b/docs/sophia.md index 754f976..308108f 100644 --- a/docs/sophia.md +++ b/docs/sophia.md @@ -1,1195 +1 @@ - - -# The Sophia Language -An Æternity BlockChain Language - -The Sophia is a language in the ML family. It is strongly typed and has -restricted mutable state. - -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 - -The main unit of code in Sophia is the *contract*. - -- A contract implementation, or simply a contract, is the code for a - smart contract and consists of a list of types, entrypoints and local - functions. Only the entrypoints can be called from outside the contract. -- A contract instance is an entity living on the block chain (or in a state - channel). Each instance has an address that can be used to call its - entrypoints, either from another contract or in a call transaction. -- A contract may define a type `state` encapsulating its local - state. When creating a new contract the `init` entrypoint is executed and the - state is initialized to its return value. - -The language offers some primitive functions to interact with the blockchain and contracts. -Please refer to the [Chain](sophia_stdlib.md#Chain), [Contract](sophia_stdlib.md#Contract) -and the [Call](sophia_stdlib.md#Call) namespaces in the documentation. - -#### Calling other contracts - -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, - -```sophia -// A contract type -contract interface VotingType = - entrypoint vote : string => unit -``` - -Now given contract address of type `VotingType` you can call the `vote` -entrypoint of that contract: - -```sophia -contract VoteTwice = - entrypoint voteTwice(v : VotingType, alt : string) = - v.vote(alt) - v.vote(alt) -``` - -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: - -```sophia - entrypoint voteTwice(v : VotingType, fee : int, alt : string) = - v.vote(value = fee, alt) - v.vote(value = fee, alt) -``` - -Named arguments can be given in any order. - -Note that reentrant calls are not permitted. In other words, when calling -another contract it cannot call you back (directly or indirectly). - -To construct a value of a contract type you can give a contract address literal -(for instance `ct_2gPXZnZdKU716QBUFKaT4VdBZituK93KLvHJB3n4EnbrHHw4Ay`), or -convert an account address to a contract address using `Address.to_contract`. -Note that if the contract does not exist, or it doesn't have the entrypoint, or -the type of the entrypoint does not match the stated contract type, the call -fails. - -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 - -```sophia - entrypoint pay(v : VotingType, amount : int) = - Chain.spend(v.address, amount) -``` - -#### Protected contract calls - -If a contract call fails for any reason (for instance, the remote contract -crashes or runs out of gas, or the entrypoint doesn't exist or has the wrong -type) the parent call also fails. To make it possible to recover from failures, -contract calls takes a named argument `protected : bool` (default `false`). - -The protected argument must be a literal boolean, and when set to `true` -changes the type of the contract call, wrapping the result in an `option` type. -If the call fails the result is `None`, otherwise it's `Some(r)` where `r` is -the return value of the call. - -```sophia -contract interface VotingType = - entrypoint : vote : string => unit - -contract Voter = - entrypoint tryVote(v : VotingType, alt : string) = - switch(v.vote(alt, protected = true) : option(unit)) - None => "Voting failed" - Some(_) => "Voting successful" -``` - -Any gas that was consumed by the contract call before the failure stays -consumed, which means that in order to protect against the remote contract -running out of gas it is necessary to set a gas limit using the `gas` argument. -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 6.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 = - stateful 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. - -- Each contract defines a type `state` encapsulating its mutable state. - The type `state` defaults to the `unit`. -- The initial state of a contract is computed by the contract's `init` - function. The `init` function is *pure* and returns the initial state as its - return value. - If the type `state` is `unit`, the `init` function defaults to returning the value `()`. - At contract creation time, the `init` function is executed and - its result is stored as the contract state. -- The value of the state is accessible from inside the contract - through an implicitly bound variable `state`. -- State updates are performed by calling a function `put : state => unit`. -- Aside from the `put` function (and similar functions for transactions - and events), the language is purely functional. -- Functions modifying the state need to be annotated with the `stateful` keyword (see below). - -To make it convenient to update parts of a deeply nested state Sophia -provides special syntax for map/record updates. - -#### Stateful functions - -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, - -```sophia - stateful entrypoint set_state(s : state) = - put(s) -``` - -Without the `stateful` annotation the compiler does not allow the call to -`put`. A `stateful` annotation is required to - -* Use a stateful primitive function. These are - - `put` - - `Chain.spend` - - `Oracle.register` - - `Oracle.query` - - `Oracle.respond` - - `Oracle.extend` - - `AENS.preclaim` - - `AENS.claim` - - `AENS.transfer` - - `AENS.revoke` - - `AENS.update` -* Call a `stateful` function in the current contract -* Call another contract with a non-zero `value` argument. - -A `stateful` annotation *is not* required to - -* Read the contract state. -* Issue an event using the `event` function. -* Call another contract with `value = 0`, even if the called function is stateful. - -### Payable - -#### Payable contracts - -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`: - -```sophia -// A payable contract -payable contract ExampleContract = - stateful entrypoint do_stuff() = ... -``` - -If in doubt, it is possible to check if an address is payable using -`Address.is_payable(addr)`. - -#### Payable entrypoints - -A contract entrypoint is by default *not* payable. Any call to such a function -(either a [Remote call](#calling-other-contracts) or a contract call transaction) -that has a non-zero `value` will fail. Contract entrypoints that should be called -with a non-zero value should be declared `payable`. - -```sophia -payable stateful entrypoint buy(to : address) = - if(Call.value > 42) - transfer_item(to) - else - abort("Value too low") -``` - -Note: In the Aeternity VM (AEVM) contracts and entrypoints were by default -payable until the Lima release. - -### Namespaces - -Code can be split into libraries using the `namespace` construct. Namespaces -can appear at the top-level and can contain type and function definitions, but -not entrypoints. Outside the namespace you can refer to the (non-private) names -by qualifying them with the namespace (`Namespace.name`). -For example, - -``` -namespace Library = - type number = int - function inc(x : number) : number = x + 1 - -contract MyContract = - entrypoint plus2(x) : Library.number = - Library.inc(Library.inc(x)) -``` - -Functions in namespaces have access to the same environment (including the -`Chain`, `Call`, and `Contract`, builtin namespaces) as function in a contract, -with the exception of `state`, `put` and `Chain.event` since these are -dependent on the specific state and event types of the contract. - -### Splitting code over multiple files - - -Code from another file can be included in a contract using an `include` -statement. These must appear at the top-level (outside the main contract). The -included file can contain one or more namespaces and abstract contracts. For -example, if the file `library.aes` contains - -``` -namespace Library = - function inc(x) = x + 1 -``` - -you can use it from another file using an `include`: - -``` -include "library.aes" -contract MyContract = - entrypoint plus2(x) = Library.inc(Library.inc(x)) -``` - -This behaves as if the contents of `library.aes` was textually inserted into -the file, except that error messages will refer to the original source -locations. The language will try to include each file at most one time automatically, -so even cyclic includes should be working without any special tinkering. - -### Standard library - -Sophia offers [standard library](sophia_stdlib.md) which exposes some -primitive operations and some higher level utilities. The builtin -namespaces like `Chain`, `Contract`, `Map` -are included by default and are supported internally by the compiler. -Others like `List`, `Frac`, `Option` need to be manually included using the -`include` directive. For example -``` -include "List.aes" -include "Pair.aes" --- Map is already there! - -namespace C = - entrypoint keys(m : map('a, 'b)) : list('a) = - List.map(Pair.fst, (Map.to_list(m))) -``` - -### Types -Sophia has the following types: - -| Type | Description | Example | -|----------------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------| -| int | A 2-complement integer | ```-1``` | -| address | Aeternity address, 32 bytes | ```Call.origin``` | -| bool | A Boolean | ```true``` | -| bits | A bit field | ```Bits.none``` | -| bytes(n) | A byte array with `n` bytes | ```#fedcba9876543210``` | -| string | An array of bytes | ```"Foo"``` | -| list | A homogeneous immutable singly linked list. | ```[1, 2, 3]``` | -| ('a, 'b) => 'c | A function. Parentheses can be skipped if there is only one argument | ```(x : int, y : int) => x + y``` | -| tuple | An ordered heterogeneous array | ```(42, "Foo", true)``` | -| record | An immutable key value store with fixed key names and typed values | ``` record balance = { owner: address, value: int } ``` | -| map | An immutable key value store with dynamic mapping of keys of one type to values of one type | ```type accounts = map(string, address)``` | -| option('a) | An optional value either None or Some('a) | ```Some(42)``` | -| state | A user defined type holding the contract state | ```record state = { owner: address, magic_key: bytes(4) }``` | -| event | An append only list of blockchain events (or log entries) | ```datatype event = EventX(indexed int, string)``` | -| hash | A 32-byte hash - equivalent to `bytes(32)` | | -| signature | A signature - equivalent to `bytes(64)` | | -| Chain.ttl | Time-to-live (fixed height or relative to current block) | ```FixedTTL(1050)``` ```RelativeTTL(50)``` | -| oracle('a, 'b) | And oracle answering questions of type 'a with answers of type 'b | ```Oracle.register(acct, qfee, ttl)``` | -| oracle_query('a, 'b) | A specific oracle query | ```Oracle.query(o, q, qfee, qttl, rttl)``` | -| contract | A user defined, typed, contract address | ```function call_remote(r : RemoteContract) = r.fun()``` | - -### Literals -| Type | Constant/Literal example(s) | -| ---------- | ------------------------------- | -| int | `-1`, `2425`, `4598275923475723498573485768` | -| address | `ak_2gx9MEFxKvY9vMG5YnqnXWv1hCsX7rgnfvBLJS4aQurustR1rt` | -| bool | `true`, `false` | -| bits | `Bits.none`, `Bits.all` | -| bytes(8) | `#fedcba9876543210` | -| string | `"This is a string"` | -| list | `[1, 2, 3]`, `[(true, 24), (false, 19), (false, -42)]` | -| tuple | `(42, "Foo", true)` | -| record | `{ owner = Call.origin, value = 100000000 }` | -| map | `{["foo"] = 19, ["bar"] = 42}`, `{}` | -| option(int) | `Some(42)`, `None` | -| state | `state{ owner = Call.origin, magic_key = #a298105f }` | -| event | `EventX(0, "Hello")` | -| hash | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | -| signature | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | -| Chain.ttl | `FixedTTL(1050)`, `RelativeTTL(50)` | -| oracle('a, 'b) | `ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5` | -| oracle_query('a, 'b) | `oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY` | -| contract | `ct_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ` | - -### Arithmetic - -Sophia integers (`int`) are represented by 256-bit (AEVM) or arbitrary-sized (FATE) signed words and supports the following -arithmetic operations: -- addition (`x + y`) -- subtraction (`x - y`) -- multiplication (`x * y`) -- division (`x / y`), truncated towards zero -- remainder (`x mod y`), satisfying `y * (x / y) + x mod y == x` for non-zero `y` -- exponentiation (`x ^ y`) - -All operations are *safe* with respect to overflow and underflow. On AEVM they behave as the corresponding -operations on arbitrary-size integers and fail with `arithmetic_error` if the -result cannot be represented by a 256-bit signed word. For example, `2 ^ 255` -fails rather than wrapping around to -2²⁵⁵. - -The division and modulo operations also throw an arithmetic error if the -second argument is zero. - -### Bit fields - -Sophia integers do not support bit arithmetic. Instead there is a separate -type `bits`. See the standard library [documentation](sophia_stdlib.md#Bits). - -On the AEVM a bit field is represented by a 256-bit word and reading or writing -a bit outside the 0..255 range fails with an `arithmetic_error`. On FATE a bit -field can be of arbitrary size (but it is still represented by the -corresponding integer, so setting very high bits can be expensive). - -### Type aliases - -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) -``` - -A type alias and its definition can be used interchangeably. Sophia does not support -higher-kinded types, meaning that following type alias is invalid: `type wrap('f, 'a) = 'f('a)` - -### Algebraic data types - -Sophia supports algebraic data types (variant types) and pattern matching. Data -types are declared by giving a list of constructors with -their respective arguments. For instance, - -``` -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) - Right(_) => None - Both(x, _) => Some(x) -``` - -or directly in the left-hand side: -```sophia -function - get_left : one_or_both('a, 'b) => option('a) - get_left(Left(x)) = Some(x) - get_left(Right(_)) = None - get_left(Both(x, _)) = Some(x) -``` - -*NOTE: Data types cannot currently be recursive.* - -### Lists - -A Sophia list is a dynamically sized, homogenous, immutable, singly -linked list. A list is constructed with the syntax `[1, 2, 3]`. The -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)) -``` - -New elements can be prepended to the front of a list with the `::` -operator. So `42 :: [1, 2, 3]` returns the list `[42, 1, 2, 3]`. The -concatenation operator `++` appends its second argument to its first -and returns the resulting list. So concatenating two lists -`[1, 22, 33] ++ [10, 18, 55]` returns the list `[1, 22, 33, 10, 18, 55]`. - -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. - - -Please refer to the [standard library](sophia_stdlib.md#List) for the predefined functionalities. - -### Maps and records - -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) } -``` - -Maps, on the other hand, can contain an arbitrary number of key-value bindings, -but of a fixed type. The type of maps with keys of type `'k` and values of type -`'v` is written `map('k, 'v)`. The key type can be any type that does not -contain a map or a function type. - -Please refer to the [standard library](sophia_stdlib.md#Map) for the predefined functionalities. - -#### Constructing maps and records - -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} -``` -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 -``` -Looking up a non-existing key in a map results in contract execution failing. A -default value to return for non-existing keys can be provided using the syntax -`m[k = default]`. See also `Map.member` and `Map.lookup` below. - -#### Updating a value - -Record field updates are written `r{f = v}`. This creates a new record value -which is the same as `r`, but with the value of the field `f` replaced by `v`. -Similarly, `m{[k] = v}` constructs a map with the same values as `m` except -that `k` maps to `v`. It makes no difference if `m` has a mapping for `k` or -not. - -It is possible to give a name to the old value of a field or mapping in an -update: instead of `acc{ balance = acc.balance + 100 }` it is possible to write -`acc{ balance @ b = b + 100 }`, binding `b` to `acc.balance`. When giving a -name to a map value (`m{ [k] @ x = v }`), the corresponding key must be present -in the map or execution fails, but a default value can be provided: -`m{ [k = default] @ x = v }`. In this case `x` is bound to `default` if -`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 = [] } -``` - -#### Map implementation - -Internally in the VM maps are implemented as hash maps and support fast lookup -and update. Large maps can be stored in the contract state and the size of the -map does not contribute to the gas costs of a contract call reading or updating -it. - -### Strings - -There is a builtin type `string`, which can be seen as an array of bytes. -Strings can be compared for equality (`==`, `!=`), used as keys in maps and -records, and used in builtin functions `String.length`, `String.concat` and -the hash functions described below. - -Please refer to the `String` [library documentation](sophia_stdlib.md#String). - -### Chars - -There is a builtin type `char` (the underlying representation being an integer), -mainly used to manipulate strings via `String.to_list`/`String.from_list`. - -Characters can also be introduced as character literals (`'x', '+', ...). - -Please refer to the `Char` [library documentation](sophia_stdlib.md#Char). - -### Byte arrays - -Byte arrays are fixed size arrays of 8-bit integers. They are described in hexadecimal system, -for example the literal `#cafe` creates a two-element array of bytes `ca` (202) and `fe` (254) -and thus is a value of type `bytes(2)`. - -Please refer to the `Bytes` [library documentation](sophia_stdlib.md#Bytes). - - -### Cryptographic builins - -Libraries [Crypto](sophia_stdlib.md#Crypto) and [String](sophia_stdlib.md#String) provide functions to -hash objects, verify signatures etc. The `hash` is a type alias for `bytes(32)`. - -#### AEVM note -The hash functions in `String` hash strings interpreted as byte arrays, and -the `Crypto` hash functions accept an element of any (first-order) type. The -result is the hash of the binary encoding of the argument as [described -below](#encoding-sophia-values-as-binaries). Note that this means that for `s : -string`, `String.sha3(s)` and `Crypto.sha3(s)` will give different results on AEVM. - - -### Authorization interface - -When a Generalized account is authorized, the authorization function needs -access to the transaction and the transaction hash for the wrapped transaction. (A `GAMetaTx` -wrapping a transaction.) The transaction and the transaction hash is available in the primitive -`Auth.tx` and `Auth.tx_hash` respectively, they are *only* available during authentication if invoked by a -normal contract call they return `None`. - -### Oracle interface -You can attach an oracle to the current contract and you can interact with oracles -through the Oracle interface. - -For a full description of how Oracle works see -[Oracles](https://github.com/aeternity/protocol/blob/master/oracles/oracles.md#oracles). -For a functionality documentation refer to the [standard library](sophia_stdlib.md#Oracle). - - -#### Example - -Example for an oracle answering questions of type `string` with answers of type `int`: -```sophia -contract Oracles = - - stateful entrypoint registerOracle(acct : address, - sign : signature, // Signed network id + oracle address + contract address - qfee : int, - ttl : Chain.ttl) : oracle(string, int) = - Oracle.register(acct, signature = sign, qfee, ttl) - - entrypoint queryFee(o : oracle(string, int)) : int = - Oracle.query_fee(o) - - payable stateful entrypoint createQuery(o : oracle_query(string, int), - q : string, - qfee : int, - qttl : Chain.ttl, - rttl : int) : oracle_query(string, int) = - require(qfee =< Call.value, "insufficient value for qfee") - Oracle.query(o, q, qfee, qttl, RelativeTTL(rttl)) - - stateful entrypoint extendOracle(o : oracle(string, int), - ttl : Chain.ttl) : unit = - Oracle.extend(o, ttl) - - stateful entrypoint signExtendOracle(o : oracle(string, int), - sign : signature, // Signed network id + oracle address + contract address - ttl : Chain.ttl) : unit = - Oracle.extend(o, signature = sign, ttl) - - stateful entrypoint respond(o : oracle(string, int), - q : oracle_query(string, int), - sign : signature, // Signed network id + oracle query id + contract address - r : int) = - Oracle.respond(o, q, signature = sign, r) - - entrypoint getQuestion(o : oracle(string, int), - q : oracle_query(string, int)) : string = - Oracle.get_question(o, q) - - entrypoint hasAnswer(o : oracle(string, int), - q : oracle_query(string, int)) = - switch(Oracle.get_answer(o, q)) - None => false - Some(_) => true - - entrypoint getAnswer(o : oracle(string, int), - q : oracle_query(string, int)) : option(int) = - Oracle.get_answer(o, q) -``` - -#### Sanity checks - -When an Oracle literal is passed to a contract, no deep checks are performed. -For extra safety [Oracle.check](sophia_stdlib.md#check) and [Oracle.check_query](sophia_stdlib.md#check_query) -functions are provided. - -### AENS interface - -Contracts can interact with the -[Aeternity Naming System](https://github.com/aeternity/protocol/blob/master/AENS.md). -For this purpose the [AENS](sophia_stdlib.md#AENS) library was exposed. - -#### Example - -In this example we assume that the name `name` already exists, and is owned by -an account with address `addr`. In order to allow a contract `ct` to handle -`name` the account holder needs to create a -[signature](#delegation-signature) `sig` of `addr | name.hash | ct.address`. - -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 => () - Some(AENS.Name(_, FixedTTL(expiry), _)) => - if(Chain.block_height + 1000 > expiry) - AENS.update(addr, name, Some(RelativeTTL(50000)), None, None, signature = sig) -``` - -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)) - None => () - Some(AENS.Name(_, _, ptrs)) => - AENS.update(addr, name, None, None, Some(ptrs{[key] = pt}), signature = sig) - - stateful entrypoint delete_key(addr : address, name : string, - key : string, sig : signature) = - switch(AENS.lookup(name)) - None => () - Some(AENS.Name(_, _, ptrs)) => - let ptrs = Map.delete(key, ptrs) - AENS.update(addr, name, None, None, Some(ptrs), signature = sig) -``` - -*Note:* From the Iris hardfork more strict rules apply for AENS pointers, when -a Sophia contract lookup or update (bad) legacy pointers, the bad keys are -automatically removed so they will not appear in the pointers map. - -### Events - -Sophia contracts log structured messages to an event log in the resulting -blockchain transaction. The event log is quite similar to [Events in -Solidity](https://solidity.readthedocs.io/en/v0.4.24/contracts.html#events). -Events are further discussed in the [protocol](https://github.com/aeternity/protocol/blob/master/contracts/events.md). - - -To use events a contract must declare a datatype `event`, and events are then -logged using the `Chain.event` function: - -``` - datatype event - = Event1(int, int, string) - | Event2(string, address) - - Chain.event(e : event) : unit -``` - -The event can have 0-3 *indexed* fields, and an optional *payload* field. A -field is indexed if it fits in a 32-byte word, i.e. -- `bool` -- `int` -- `bits` -- `address` -- `oracle(_, _)` -- `oracle_query(_, _)` -- contract types -- `bytes(n)` for `n` ≤ 32, in particular `hash` - -The payload field must be either a string or a byte array of more than 32 bytes. -The fields can appear in any order. - -*NOTE:* Indexing is not part of the core aeternity node. - -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(Event1(42, 34, "foo")) - Chain.event(Event2("This is not indexed", Contract.address)) -``` - -#### Argument order - -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) - -... - -Chain.event(AnotherEvent("This is not indexed", Contract.address)) -``` -would yield exactly the same result in the example above! - -### Compiler pragmas - -To enforce that a contract is only compiled with specific versions of the -Sophia compiler, you can give one or more `@compiler` pragmas at the -top-level (typically at the beginning) of a file. For instance, to enforce that -a contract is compiled with version 4.3 of the compiler you write - -``` -@compiler >= 4.3 -@compiler < 4.4 -``` - -Valid operators in compiler pragmas are `<`, `=<`, `==`, `>=`, and `>`. Version -numbers are given as a sequence of non-negative integers separated by dots. -Trailing zeros are ignored, so `4.0.0 == 4`. If a constraint is violated an -error is reported and compilation fails. - -### Exceptions - -Contracts can fail with an (uncatchable) exception using the built-in function - -``` -abort(reason : string) : 'a -``` - -Calling abort causes the top-level call transaction to return an error result -containing the `reason` string. Only the gas used up to and including the abort -call is charged. This is different from termination due to a crash which -consumes all available gas. - -For convenience the following function is also built-in: - -``` -function require(b : bool, err : string) = - if(!b) abort(err) -``` - -## Syntax - -### Lexical syntax - -#### Comments - -Single line comments start with `//` and block comments are enclosed in `/*` -and `*/` and can be nested. - -#### Keywords - -``` -contract elif else entrypoint false function if import include let mod namespace -private payable stateful switch true type record datatype main interface -``` - -#### Tokens - -- `Id = [a-z_][A-Za-z0-9_']*` identifiers start with a lower case letter. -- `Con = [A-Z][A-Za-z0-9_']*` constructors start with an upper case letter. -- `QId = (Con\.)+Id` qualified identifiers (e.g. `Map.member`) -- `QCon = (Con\.)+Con` qualified constructor -- `TVar = 'Id` type variable (e.g `'a`, `'b`) -- `Int = [0-9]+(_[0-9]+)*|0x[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*` integer literal with optional `_` separators -- `Bytes = #[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*` byte array literal with optional `_` separators -- `String` string literal enclosed in `"` with escape character `\` -- `Char` character literal enclosed in `'` with escape character `\` -- `AccountAddress` base58-encoded 32 byte account pubkey with `ak_` prefix -- `ContractAddress` base58-encoded 32 byte contract address with `ct_` prefix -- `OracleAddress` base58-encoded 32 byte oracle address with `ok_` prefix -- `OracleQueryId` base58-encoded 32 byte oracle query id with `oq_` prefix - -Valid string escape codes are - -| Escape | ASCII | | -|---------------|-------------|---| -| `\b` | 8 | | -| `\t` | 9 | | -| `\n` | 10 | | -| `\v` | 11 | | -| `\f` | 12 | | -| `\r` | 13 | | -| `\e` | 27 | | -| `\xHexDigits` | *HexDigits* | | - - -See the [identifier encoding scheme](https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md) for the -details on the base58 literals. - -### Layout blocks - -Sophia uses Python-style layout rules to group declarations and statements. A -layout block with more than one element must start on a separate line and be -indented more than the currently enclosing layout block. Blocks with a single -element can be written on the same line as the previous token. - -Each element of the block must share the same indentation and no part of an -element may be indented less than the indentation of the block. For instance - -``` -contract Layout = - function foo() = 0 // no layout - function bar() = // layout block starts on next line - let x = foo() // indented more than 2 spaces - x - + 1 // the '+' is indented more than the 'x' -``` - -### Notation - -In describing the syntax below, we use the following conventions: -- Upper-case identifiers denote non-terminals (like `Expr`) or terminals with - some associated value (like `Id`). -- Keywords and symbols are enclosed in single quotes: `'let'` or `'='`. -- Choices are separated by vertical bars: `|`. -- Optional elements are enclosed in `[` square brackets `]`. -- `(` Parentheses `)` are used for grouping. -- Zero or more repetitions are denoted by a postfix `*`, and one or more - repetitions by a `+`. -- `Block(X)` denotes a layout block of `X`s. -- `Sep(X, S)` is short for `[X (S X)*]`, i.e. a possibly empty sequence of `X`s - separated by `S`s. -- `Sep1(X, S)` is short for `X (S X)*`, i.e. same as `Sep`, but must not be empty. - - -### Declarations - -A Sophia file consists of a sequence of *declarations* in a layout block. - -```c -File ::= Block(TopDecl) - -TopDecl ::= ['payable'] 'contract' Con '=' Block(Decl) - | 'namespace' Con '=' Block(Decl) - | '@compiler' PragmaOp Version - | 'include' String - -Decl ::= 'type' Id ['(' TVar* ')'] '=' TypeAlias - | 'record' Id ['(' TVar* ')'] '=' RecordType - | 'datatype' Id ['(' TVar* ')'] '=' DataType - | (EModifier* 'entrypoint' | FModifier* 'function') Block(FunDecl) - -FunDecl ::= Id ':' Type // Type signature - | Id Args [':' Type] '=' Block(Stmt) // Definition - -PragmaOp ::= '<' | '=<' | '==' | '>=' | '>' -Version ::= Sep1(Int, '.') - -EModifier ::= 'payable' | 'stateful' -FModifier ::= 'stateful' | 'private' - -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 -``` - -There are three forms of type declarations: type aliases (declared with the -`type` keyword), record type definitions (`record`) and data type definitions -(`datatype`): - -```c -TypeAlias ::= Type -RecordType ::= '{' Sep(FieldType, ',') '}' -DataType ::= Sep1(ConDecl, '|') - -FieldType ::= Id ':' Type -ConDecl ::= Con ['(' Sep1(Type, ',') ')'] -``` - -For example, -``` -record point('a) = {x : 'a, y : 'a} -datatype shape('a) = Circle(point('a), 'a) | Rect(point('a), point('a)) -type int_shape = shape(int) -``` - -### Types - -```c -Type ::= Domain '=>' Type // Function type - | Type '(' Sep(Type, ',') ')' // Type application - | '(' Type ')' // Parens - | 'unit' | Sep(Type, '*') // Tuples - | Id | QId | TVar - -Domain ::= Type // Single argument - | '(' Sep(Type, ',') ')' // Multiple arguments -``` - -The function type arrow associates to the right. - -Example, -``` -'a => list('a) => (int * list('a)) -``` - -### Statements - -Function bodies are blocks of *statements*, where a statement is one of the following - -```c -Stmt ::= 'switch' '(' Expr ')' Block(Case) - | 'if' '(' Expr ')' Block(Stmt) - | 'elif' '(' Expr ')' Block(Stmt) - | 'else' Block(Stmt) - | 'let' LetDef - | Expr - -LetDef ::= Id Args [':' Type] '=' Block(Stmt) // Function definition - | Pattern '=' Block(Stmt) // Value definition - -Case ::= Pattern '=>' Block(Stmt) -Pattern ::= Expr -``` - -`if` statements can be followed by zero or more `elif` statements and an optional final `else` statement. For example, - -``` -let x : int = 4 -switch(f(x)) - None => 0 - Some(y) => - if(y > 10) - "too big" - elif(y < 3) - "too small" - else - "just right" -``` - -### Expressions - -```c -Expr ::= '(' LamArgs ')' '=>' Block(Stmt) // Anonymous function (x) => x + 1 - | 'if' '(' Expr ')' Expr 'else' Expr // If expression if(x < y) y else x - | Expr ':' Type // Type annotation 5 : int - | Expr BinOp Expr // Binary operator x + y - | UnOp Expr // Unary operator ! b - | Expr '(' Sep(Expr, ',') ')' // Application f(x, y) - | Expr '.' Id // Projection state.x - | Expr '[' Expr ']' // Map lookup map[key] - | Expr '{' Sep(FieldUpdate, ',') '}' // Record or map update r{ fld[key].x = y } - | '[' Sep(Expr, ',') ']' // List [1, 2, 3] - | '[' Expr '|' Sep(Generator, ',') ']' - // List comprehension [k | x <- [1], if (f(x)), let k = x+1] - | '[' Expr '..' Expr ']' // List range [1..n] - | '{' Sep(FieldUpdate, ',') '}' // Record or map value {x = 0, y = 1}, {[key] = val} - | '(' Expr ')' // Parens (1 + 2) * 3 - | Id | Con | QId | QCon // Identifiers x, None, Map.member, AELib.Token - | Int | Bytes | String | Char // Literals 123, 0xff, #00abc123, "foo", '%' - | AccountAddress | ContractAddress // Chain identifiers - | OracleAddress | OracleQueryId // Chain identifiers - -Generator ::= Pattern '<-' Expr // Generator - | 'if' '(' Expr ')' // Guard - | LetDef // Definition - -LamArgs ::= '(' Sep(LamArg, ',') ')' -LamArg ::= Id [':' Type] - -FieldUpdate ::= Path '=' Expr -Path ::= Id // Record field - | '[' Expr ']' // Map key - | Path '.' Id // Nested record field - | Path '[' Expr ']' // Nested map key - -BinOp ::= '||' | '&&' | '<' | '>' | '=<' | '>=' | '==' | '!=' - | '::' | '++' | '+' | '-' | '*' | '/' | 'mod' | '^' -UnOp ::= '-' | '!' -``` - -### Operators types - -| Operators | Type -| --- | --- -| `-` `+` `*` `/` `mod` `^` | arithmetic operators -| `!` `&&` `\|\|` | logical operators -| `==` `!=` `<` `>` `=<` `>=` | comparison operators -| `::` `++` | list operators - -### Operator precendences - -In order of highest to lowest precedence. - -| Operators | Associativity -| --- | --- -| `!` | right -| `^` | left -| `*` `/` `mod` | left -| `-` (unary) | right -| `+` `-` | left -| `::` `++` | right -| `<` `>` `=<` `>=` `==` `!=` | none -| `&&` | right -| `\|\|` | right - -## Examples - -```sophia -/* - * A simple crowd-funding example - */ -contract FundMe = - - record spend_args = { recipient : address, - amount : int } - - record state = { contributions : map(address, int), - total : int, - beneficiary : address, - deadline : int, - goal : int } - - stateful function spend(args : spend_args) = - Chain.spend(args.recipient, args.amount) - - entrypoint init(beneficiary, deadline, goal) : state = - { contributions = {}, - beneficiary = beneficiary, - deadline = deadline, - total = 0, - goal = goal } - - function is_contributor(addr) = - Map.member(addr, state.contributions) - - stateful entrypoint contribute() = - if(Chain.block_height >= state.deadline) - spend({ recipient = Call.caller, amount = Call.value }) // Refund money - false - else - let amount = - switch(Map.lookup(Call.caller, state.contributions)) - None => Call.value - Some(n) => n + Call.value - put(state{ contributions[Call.caller] = amount, - total @ tot = tot + Call.value }) - true - - stateful entrypoint withdraw() = - if(Chain.block_height < state.deadline) - abort("Cannot withdraw before deadline") - if(Call.caller == state.beneficiary) - withdraw_beneficiary() - elif(is_contributor(Call.caller)) - withdraw_contributor() - else - abort("Not a contributor or beneficiary") - - stateful function withdraw_beneficiary() = - require(state.total >= state.goal, "Project was not funded") - spend({recipient = state.beneficiary, - amount = Contract.balance }) - - stateful function withdraw_contributor() = - if(state.total >= state.goal) - abort("Project was funded") - let to = Call.caller - spend({recipient = to, - amount = state.contributions[to]}) - put(state{ contributions @ c = Map.delete(to, c) }) -``` - -### Delegation signature - -Some chain operations (`Oracle.` and `AENS.`) have an -optional delegation signature. This is typically used when a user/accounts -would like to allow a contract to act on it's behalf. The exact data to be -signed varies for the different operations, but in all cases you should prepend -the signature data with the `network_id` (`ae_mainnet` for the Aeternity mainnet, etc.). +This file has been moved [here](sophia_features.md) \ No newline at end of file diff --git a/docs/sophia_examples.md b/docs/sophia_examples.md new file mode 100644 index 0000000..fab9285 --- /dev/null +++ b/docs/sophia_examples.md @@ -0,0 +1,73 @@ +# Contract examples + +## Crowdfunding +```sophia +/* + * A simple crowd-funding example + */ +contract FundMe = + + record spend_args = { recipient : address, + amount : int } + + record state = { contributions : map(address, int), + total : int, + beneficiary : address, + deadline : int, + goal : int } + + stateful function spend(args : spend_args) = + Chain.spend(args.recipient, args.amount) + + entrypoint init(beneficiary, deadline, goal) : state = + { contributions = {}, + beneficiary = beneficiary, + deadline = deadline, + total = 0, + goal = goal } + + function is_contributor(addr) = + Map.member(addr, state.contributions) + + stateful entrypoint contribute() = + if(Chain.block_height >= state.deadline) + spend({ recipient = Call.caller, amount = Call.value }) // Refund money + false + else + let amount = + switch(Map.lookup(Call.caller, state.contributions)) + None => Call.value + Some(n) => n + Call.value + put(state{ contributions[Call.caller] = amount, + total @ tot = tot + Call.value }) + true + + stateful entrypoint withdraw() = + if(Chain.block_height < state.deadline) + abort("Cannot withdraw before deadline") + if(Call.caller == state.beneficiary) + withdraw_beneficiary() + elif(is_contributor(Call.caller)) + withdraw_contributor() + else + abort("Not a contributor or beneficiary") + + stateful function withdraw_beneficiary() = + require(state.total >= state.goal, "Project was not funded") + spend({recipient = state.beneficiary, + amount = Contract.balance }) + + stateful function withdraw_contributor() = + if(state.total >= state.goal) + abort("Project was funded") + let to = Call.caller + spend({recipient = to, + amount = state.contributions[to]}) + put(state{ contributions @ c = Map.delete(to, c) }) +``` + +## Repositories +This is a list with repositories that include smart contracts written in Sophia: + +- [aepp-sophia-examples](https://github.com/aeternity/aepp-sophia-examples) + - A repository that contains lots of different examples. The functionality of these examples is - to some extent - also covered by tests written in JavaScript. \ No newline at end of file diff --git a/docs/sophia_features.md b/docs/sophia_features.md new file mode 100644 index 0000000..6be7ac4 --- /dev/null +++ b/docs/sophia_features.md @@ -0,0 +1,783 @@ +# Features +## Contracts + +The main unit of code in Sophia is the *contract*. + +- A contract implementation, or simply a contract, is the code for a + smart contract and consists of a list of types, entrypoints and local + functions. Only the entrypoints can be called from outside the contract. +- A contract instance is an entity living on the block chain (or in a state + channel). Each instance has an address that can be used to call its + entrypoints, either from another contract or in a call transaction. +- A contract may define a type `state` encapsulating its local + state. When creating a new contract the `init` entrypoint is executed and the + state is initialized to its return value. + +The language offers some primitive functions to interact with the blockchain and contracts. +Please refer to the [Chain](sophia_stdlib.md#chain), [Contract](sophia_stdlib.md#contract) +and the [Call](sophia_stdlib.md#call) namespaces in the documentation. + +### Calling other contracts + +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, + +```sophia +// A contract type +contract interface VotingType = + entrypoint vote : string => unit +``` + +Now given contract address of type `VotingType` you can call the `vote` +entrypoint of that contract: + +```sophia +contract VoteTwice = + entrypoint voteTwice(v : VotingType, alt : string) = + v.vote(alt) + v.vote(alt) +``` + +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: + +```sophia + entrypoint voteTwice(v : VotingType, fee : int, alt : string) = + v.vote(value = fee, alt) + v.vote(value = fee, alt) +``` + +Named arguments can be given in any order. + +Note that reentrant calls are not permitted. In other words, when calling +another contract it cannot call you back (directly or indirectly). + +To construct a value of a contract type you can give a contract address literal +(for instance `ct_2gPXZnZdKU716QBUFKaT4VdBZituK93KLvHJB3n4EnbrHHw4Ay`), or +convert an account address to a contract address using `Address.to_contract`. +Note that if the contract does not exist, or it doesn't have the entrypoint, or +the type of the entrypoint does not match the stated contract type, the call +fails. + +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 + +```sophia + entrypoint pay(v : VotingType, amount : int) = + Chain.spend(v.address, amount) +``` + +### Protected contract calls + +If a contract call fails for any reason (for instance, the remote contract +crashes or runs out of gas, or the entrypoint doesn't exist or has the wrong +type) the parent call also fails. To make it possible to recover from failures, +contract calls takes a named argument `protected : bool` (default `false`). + +The protected argument must be a literal boolean, and when set to `true` +changes the type of the contract call, wrapping the result in an `option` type. +If the call fails the result is `None`, otherwise it's `Some(r)` where `r` is +the return value of the call. + +```sophia +contract interface VotingType = + entrypoint : vote : string => unit + +contract Voter = + entrypoint tryVote(v : VotingType, alt : string) = + switch(v.vote(alt, protected = true) : option(unit)) + None => "Voting failed" + Some(_) => "Voting successful" +``` + +Any gas that was consumed by the contract call before the failure stays +consumed, which means that in order to protect against the remote contract +running out of gas it is necessary to set a gas limit using the `gas` argument. +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 6.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 + +```sophia +contract IntHolder = + type state = int + entrypoint init(x) = x + entrypoint get() = state + +main contract IntHolderFactory = + stateful 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. + +- Each contract defines a type `state` encapsulating its mutable state. + The type `state` defaults to the `unit`. +- The initial state of a contract is computed by the contract's `init` + function. The `init` function is *pure* and returns the initial state as its + return value. + If the type `state` is `unit`, the `init` function defaults to returning the value `()`. + At contract creation time, the `init` function is executed and + its result is stored as the contract state. +- The value of the state is accessible from inside the contract + through an implicitly bound variable `state`. +- State updates are performed by calling a function `put : state => unit`. +- Aside from the `put` function (and similar functions for transactions + and events), the language is purely functional. +- Functions modifying the state need to be annotated with the `stateful` keyword (see below). + +To make it convenient to update parts of a deeply nested state Sophia +provides special syntax for map/record updates. + +### Stateful functions + +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, + +```sophia + stateful entrypoint set_state(s : state) = + put(s) +``` + +Without the `stateful` annotation the compiler does not allow the call to +`put`. A `stateful` annotation is required to + +* Use a stateful primitive function. These are + - `put` + - `Chain.spend` + - `Oracle.register` + - `Oracle.query` + - `Oracle.respond` + - `Oracle.extend` + - `AENS.preclaim` + - `AENS.claim` + - `AENS.transfer` + - `AENS.revoke` + - `AENS.update` +* Call a `stateful` function in the current contract +* Call another contract with a non-zero `value` argument. + +A `stateful` annotation *is not* required to + +* Read the contract state. +* Issue an event using the `event` function. +* Call another contract with `value = 0`, even if the called function is stateful. + +## Payable + +### Payable contracts + +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`: + +```sophia +// A payable contract +payable contract ExampleContract = + stateful entrypoint do_stuff() = ... +``` + +If in doubt, it is possible to check if an address is payable using +`Address.is_payable(addr)`. + +### Payable entrypoints + +A contract entrypoint is by default *not* payable. Any call to such a function +(either a [Remote call](#calling-other-contracts) or a contract call transaction) +that has a non-zero `value` will fail. Contract entrypoints that should be called +with a non-zero value should be declared `payable`. + +```sophia +payable stateful entrypoint buy(to : address) = + if(Call.value > 42) + transfer_item(to) + else + abort("Value too low") +``` + +Note: In the æternity VM (AEVM) contracts and entrypoints were by default +payable until the Lima release. + +## Namespaces + +Code can be split into libraries using the `namespace` construct. Namespaces +can appear at the top-level and can contain type and function definitions, but +not entrypoints. Outside the namespace you can refer to the (non-private) names +by qualifying them with the namespace (`Namespace.name`). +For example, + +```sophia +namespace Library = + type number = int + function inc(x : number) : number = x + 1 + +contract MyContract = + entrypoint plus2(x) : Library.number = + Library.inc(Library.inc(x)) +``` + +Functions in namespaces have access to the same environment (including the +`Chain`, `Call`, and `Contract`, builtin namespaces) as function in a contract, +with the exception of `state`, `put` and `Chain.event` since these are +dependent on the specific state and event types of the contract. + +## Splitting code over multiple files + +Code from another file can be included in a contract using an `include` +statement. These must appear at the top-level (outside the main contract). The +included file can contain one or more namespaces and abstract contracts. For +example, if the file `library.aes` contains + +```sophia +namespace Library = + function inc(x) = x + 1 +``` + +you can use it from another file using an `include`: + +```sophia +include "library.aes" +contract MyContract = + entrypoint plus2(x) = Library.inc(Library.inc(x)) +``` + +This behaves as if the contents of `library.aes` was textually inserted into +the file, except that error messages will refer to the original source +locations. The language will try to include each file at most one time automatically, +so even cyclic includes should be working without any special tinkering. + +## Standard library + +Sophia offers [standard library](sophia_stdlib.md) which exposes some +primitive operations and some higher level utilities. The builtin +namespaces like `Chain`, `Contract`, `Map` +are included by default and are supported internally by the compiler. +Others like `List`, `Frac`, `Option` need to be manually included using the +`include` directive. For example +```sophia +include "List.aes" +include "Pair.aes" +-- Map is already there! + +namespace C = + entrypoint keys(m : map('a, 'b)) : list('a) = + List.map(Pair.fst, (Map.to_list(m))) +``` + +## Types +Sophia has the following types: + +| Type | Description | Example | +|----------------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| int | A 2-complement integer | ```-1``` | +| address | æternity address, 32 bytes | ```Call.origin``` | +| bool | A Boolean | ```true``` | +| bits | A bit field | ```Bits.none``` | +| bytes(n) | A byte array with `n` bytes | ```#fedcba9876543210``` | +| string | An array of bytes | ```"Foo"``` | +| list | A homogeneous immutable singly linked list. | ```[1, 2, 3]``` | +| ('a, 'b) => 'c | A function. Parentheses can be skipped if there is only one argument | ```(x : int, y : int) => x + y``` | +| tuple | An ordered heterogeneous array | ```(42, "Foo", true)``` | +| record | An immutable key value store with fixed key names and typed values | ``` record balance = { owner: address, value: int } ``` | +| map | An immutable key value store with dynamic mapping of keys of one type to values of one type | ```type accounts = map(string, address)``` | +| option('a) | An optional value either None or Some('a) | ```Some(42)``` | +| state | A user defined type holding the contract state | ```record state = { owner: address, magic_key: bytes(4) }``` | +| event | An append only list of blockchain events (or log entries) | ```datatype event = EventX(indexed int, string)``` | +| hash | A 32-byte hash - equivalent to `bytes(32)` | | +| signature | A signature - equivalent to `bytes(64)` | | +| Chain.ttl | Time-to-live (fixed height or relative to current block) | ```FixedTTL(1050)``` ```RelativeTTL(50)``` | +| oracle('a, 'b) | And oracle answering questions of type 'a with answers of type 'b | ```Oracle.register(acct, qfee, ttl)``` | +| oracle_query('a, 'b) | A specific oracle query | ```Oracle.query(o, q, qfee, qttl, rttl)``` | +| contract | A user defined, typed, contract address | ```function call_remote(r : RemoteContract) = r.fun()``` | + +## Literals +| Type | Constant/Literal example(s) | +| ---------- | ------------------------------- | +| int | `-1`, `2425`, `4598275923475723498573485768` | +| address | `ak_2gx9MEFxKvY9vMG5YnqnXWv1hCsX7rgnfvBLJS4aQurustR1rt` | +| bool | `true`, `false` | +| bits | `Bits.none`, `Bits.all` | +| bytes(8) | `#fedcba9876543210` | +| string | `"This is a string"` | +| list | `[1, 2, 3]`, `[(true, 24), (false, 19), (false, -42)]` | +| tuple | `(42, "Foo", true)` | +| record | `{ owner = Call.origin, value = 100000000 }` | +| map | `{["foo"] = 19, ["bar"] = 42}`, `{}` | +| option(int) | `Some(42)`, `None` | +| state | `state{ owner = Call.origin, magic_key = #a298105f }` | +| event | `EventX(0, "Hello")` | +| hash | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | +| signature | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | +| Chain.ttl | `FixedTTL(1050)`, `RelativeTTL(50)` | +| oracle('a, 'b) | `ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5` | +| oracle_query('a, 'b) | `oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY` | +| contract | `ct_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ` | + +## Arithmetic + +Sophia integers (`int`) are represented by 256-bit (AEVM) or arbitrary-sized (FATE) signed words and supports the following +arithmetic operations: +- addition (`x + y`) +- subtraction (`x - y`) +- multiplication (`x * y`) +- division (`x / y`), truncated towards zero +- remainder (`x mod y`), satisfying `y * (x / y) + x mod y == x` for non-zero `y` +- exponentiation (`x ^ y`) + +All operations are *safe* with respect to overflow and underflow. On AEVM they behave as the corresponding +operations on arbitrary-size integers and fail with `arithmetic_error` if the +result cannot be represented by a 256-bit signed word. For example, `2 ^ 255` +fails rather than wrapping around to -2²⁵⁵. + +The division and modulo operations also throw an arithmetic error if the +second argument is zero. + +## Bit fields + +Sophia integers do not support bit arithmetic. Instead there is a separate +type `bits`. See the standard library [documentation](sophia_stdlib.md#bits). + +On the AEVM a bit field is represented by a 256-bit word and reading or writing +a bit outside the 0..255 range fails with an `arithmetic_error`. On FATE a bit +field can be of arbitrary size (but it is still represented by the +corresponding integer, so setting very high bits can be expensive). + +## Type aliases + +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) +``` + +A type alias and its definition can be used interchangeably. Sophia does not support +higher-kinded types, meaning that following type alias is invalid: `type wrap('f, 'a) = 'f('a)` + +## Algebraic data types + +Sophia supports algebraic data types (variant types) and pattern matching. Data +types are declared by giving a list of constructors with +their respective arguments. For instance, + +```sophia +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) + Right(_) => None + Both(x, _) => Some(x) +``` + +or directly in the left-hand side: +```sophia +function + get_left : one_or_both('a, 'b) => option('a) + get_left(Left(x)) = Some(x) + get_left(Right(_)) = None + get_left(Both(x, _)) = Some(x) +``` + +*NOTE: Data types cannot currently be recursive.* + +## Lists + +A Sophia list is a dynamically sized, homogenous, immutable, singly +linked list. A list is constructed with the syntax `[1, 2, 3]`. The +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)) +``` + +New elements can be prepended to the front of a list with the `::` +operator. So `42 :: [1, 2, 3]` returns the list `[42, 1, 2, 3]`. The +concatenation operator `++` appends its second argument to its first +and returns the resulting list. So concatenating two lists +`[1, 22, 33] ++ [10, 18, 55]` returns the list `[1, 22, 33, 10, 18, 55]`. + +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. + + +Please refer to the [standard library](sophia_stdlib.md#list) for the predefined functionalities. + +## Maps and records + +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) } +``` + +Maps, on the other hand, can contain an arbitrary number of key-value bindings, +but of a fixed type. The type of maps with keys of type `'k` and values of type +`'v` is written `map('k, 'v)`. The key type can be any type that does not +contain a map or a function type. + +Please refer to the [standard library](sophia_stdlib.md#map) for the predefined functionalities. + +### Constructing maps and records + +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} +``` +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 +``` +Looking up a non-existing key in a map results in contract execution failing. A +default value to return for non-existing keys can be provided using the syntax +`m[k = default]`. See also `Map.member` and `Map.lookup` below. + +### Updating a value + +Record field updates are written `r{f = v}`. This creates a new record value +which is the same as `r`, but with the value of the field `f` replaced by `v`. +Similarly, `m{[k] = v}` constructs a map with the same values as `m` except +that `k` maps to `v`. It makes no difference if `m` has a mapping for `k` or +not. + +It is possible to give a name to the old value of a field or mapping in an +update: instead of `acc{ balance = acc.balance + 100 }` it is possible to write +`acc{ balance @ b = b + 100 }`, binding `b` to `acc.balance`. When giving a +name to a map value (`m{ [k] @ x = v }`), the corresponding key must be present +in the map or execution fails, but a default value can be provided: +`m{ [k = default] @ x = v }`. In this case `x` is bound to `default` if +`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 = [] } +``` + +### Map implementation + +Internally in the VM maps are implemented as hash maps and support fast lookup +and update. Large maps can be stored in the contract state and the size of the +map does not contribute to the gas costs of a contract call reading or updating +it. + +## Strings + +There is a builtin type `string`, which can be seen as an array of bytes. +Strings can be compared for equality (`==`, `!=`), used as keys in maps and +records, and used in builtin functions `String.length`, `String.concat` and +the hash functions described below. + +Please refer to the `String` [library documentation](sophia_stdlib.md#string). + +## Chars + +There is a builtin type `char` (the underlying representation being an integer), +mainly used to manipulate strings via `String.to_list`/`String.from_list`. + +Characters can also be introduced as character literals (`'x', '+', ...). + +Please refer to the `Char` [library documentation](sophia_stdlib.md#char). + +## Byte arrays + +Byte arrays are fixed size arrays of 8-bit integers. They are described in hexadecimal system, +for example the literal `#cafe` creates a two-element array of bytes `ca` (202) and `fe` (254) +and thus is a value of type `bytes(2)`. + +Please refer to the `Bytes` [library documentation](sophia_stdlib.md#bytes). + +## Cryptographic builtins + +Libraries [Crypto](sophia_stdlib.md#crypto) and [String](sophia_stdlib.md#string) provide functions to +hash objects, verify signatures etc. The `hash` is a type alias for `bytes(32)`. + +## Authorization interface + +When a Generalized account is authorized, the authorization function needs +access to the transaction and the transaction hash for the wrapped transaction. (A `GAMetaTx` +wrapping a transaction.) The transaction and the transaction hash is available in the primitive +`Auth.tx` and `Auth.tx_hash` respectively, they are *only* available during authentication if invoked by a +normal contract call they return `None`. + +## Oracle interface +You can attach an oracle to the current contract and you can interact with oracles +through the Oracle interface. + +For a full description of how Oracle works see +[Oracles](https://github.com/aeternity/protocol/blob/master/oracles/oracles.md#oracles). +For a functionality documentation refer to the [standard library](sophia_stdlib.md#oracle). + +### Example + +Example for an oracle answering questions of type `string` with answers of type `int`: +```sophia +contract Oracles = + + stateful entrypoint registerOracle(acct : address, + sign : signature, // Signed network id + oracle address + contract address + qfee : int, + ttl : Chain.ttl) : oracle(string, int) = + Oracle.register(acct, signature = sign, qfee, ttl) + + entrypoint queryFee(o : oracle(string, int)) : int = + Oracle.query_fee(o) + + payable stateful entrypoint createQuery(o : oracle_query(string, int), + q : string, + qfee : int, + qttl : Chain.ttl, + rttl : int) : oracle_query(string, int) = + require(qfee =< Call.value, "insufficient value for qfee") + Oracle.query(o, q, qfee, qttl, RelativeTTL(rttl)) + + stateful entrypoint extendOracle(o : oracle(string, int), + ttl : Chain.ttl) : unit = + Oracle.extend(o, ttl) + + stateful entrypoint signExtendOracle(o : oracle(string, int), + sign : signature, // Signed network id + oracle address + contract address + ttl : Chain.ttl) : unit = + Oracle.extend(o, signature = sign, ttl) + + stateful entrypoint respond(o : oracle(string, int), + q : oracle_query(string, int), + sign : signature, // Signed network id + oracle query id + contract address + r : int) = + Oracle.respond(o, q, signature = sign, r) + + entrypoint getQuestion(o : oracle(string, int), + q : oracle_query(string, int)) : string = + Oracle.get_question(o, q) + + entrypoint hasAnswer(o : oracle(string, int), + q : oracle_query(string, int)) = + switch(Oracle.get_answer(o, q)) + None => false + Some(_) => true + + entrypoint getAnswer(o : oracle(string, int), + q : oracle_query(string, int)) : option(int) = + Oracle.get_answer(o, q) +``` + +### Sanity checks + +When an Oracle literal is passed to a contract, no deep checks are performed. +For extra safety [Oracle.check](sophia_stdlib.md#check) and [Oracle.check_query](sophia_stdlib.md#check_query) +functions are provided. + +## AENS interface + +Contracts can interact with the +[æternity naming system](https://github.com/aeternity/protocol/blob/master/AENS.md). +For this purpose the [AENS](sophia_stdlib.md#aens) library was exposed. + +### Example + +In this example we assume that the name `name` already exists, and is owned by +an account with address `addr`. In order to allow a contract `ct` to handle +`name` the account holder needs to create a +[signature](#delegation-signature) `sig` of `addr | name.hash | ct.address`. + +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 => () + Some(AENS.Name(_, FixedTTL(expiry), _)) => + if(Chain.block_height + 1000 > expiry) + AENS.update(addr, name, Some(RelativeTTL(50000)), None, None, signature = sig) +``` + +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)) + None => () + Some(AENS.Name(_, _, ptrs)) => + AENS.update(addr, name, None, None, Some(ptrs{[key] = pt}), signature = sig) + + stateful entrypoint delete_key(addr : address, name : string, + key : string, sig : signature) = + switch(AENS.lookup(name)) + None => () + Some(AENS.Name(_, _, ptrs)) => + let ptrs = Map.delete(key, ptrs) + AENS.update(addr, name, None, None, Some(ptrs), signature = sig) +``` + +*Note:* From the Iris hardfork more strict rules apply for AENS pointers, when +a Sophia contract lookup or update (bad) legacy pointers, the bad keys are +automatically removed so they will not appear in the pointers map. + +## Events + +Sophia contracts log structured messages to an event log in the resulting +blockchain transaction. The event log is quite similar to [Events in +Solidity](https://solidity.readthedocs.io/en/v0.4.24/contracts.html#events). +Events are further discussed in the [protocol](https://github.com/aeternity/protocol/blob/master/contracts/events.md). + + +To use events a contract must declare a datatype `event`, and events are then +logged using the `Chain.event` function: + +```sophia + datatype event + = Event1(int, int, string) + | Event2(string, address) + + Chain.event(e : event) : unit +``` + +The event can have 0-3 *indexed* fields, and an optional *payload* field. A +field is indexed if it fits in a 32-byte word, i.e. +- `bool` +- `int` +- `bits` +- `address` +- `oracle(_, _)` +- `oracle_query(_, _)` +- contract types +- `bytes(n)` for `n` ≤ 32, in particular `hash` + +The payload field must be either a string or a byte array of more than 32 bytes. +The fields can appear in any order. + +*NOTE:* Indexing is not part of the core æternity node. + +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(Event1(42, 34, "foo")) + Chain.event(Event2("This is not indexed", Contract.address)) +``` + +### Argument order + +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) + +... + +Chain.event(AnotherEvent("This is not indexed", Contract.address)) +``` +would yield exactly the same result in the example above! + +## Compiler pragmas + +To enforce that a contract is only compiled with specific versions of the +Sophia compiler, you can give one or more `@compiler` pragmas at the +top-level (typically at the beginning) of a file. For instance, to enforce that +a contract is compiled with version 4.3 of the compiler you write + +```sophia +@compiler >= 4.3 +@compiler < 4.4 +``` + +Valid operators in compiler pragmas are `<`, `=<`, `==`, `>=`, and `>`. Version +numbers are given as a sequence of non-negative integers separated by dots. +Trailing zeros are ignored, so `4.0.0 == 4`. If a constraint is violated an +error is reported and compilation fails. + +## Exceptions + +Contracts can fail with an (uncatchable) exception using the built-in function + +```sophia +abort(reason : string) : 'a +``` + +Calling abort causes the top-level call transaction to return an error result +containing the `reason` string. Only the gas used up to and including the abort +call is charged. This is different from termination due to a crash which +consumes all available gas. + +For convenience the following function is also built-in: + +```sophia +function require(b : bool, err : string) = + if(!b) abort(err) +``` + +## Delegation signature + +Some chain operations (`Oracle.` and `AENS.`) have an +optional delegation signature. This is typically used when a user/accounts +would like to allow a contract to act on it's behalf. The exact data to be +signed varies for the different operations, but in all cases you should prepend +the signature data with the `network_id` (`ae_mainnet` for the æternity mainnet, etc.). \ No newline at end of file diff --git a/docs/sophia_stdlib.md b/docs/sophia_stdlib.md index 9a9ed5a..04421d5 100644 --- a/docs/sophia_stdlib.md +++ b/docs/sophia_stdlib.md @@ -12,40 +12,40 @@ in the scope and do not need any actions to be used, while the others require so The out-of-the-box namespaces are: -- [Bits](#Bits) -- [Bytes](#Bytes) -- [Char](#Char) -- [Int](#Int) -- [Map](#Map) -- [Address](#Address) -- [Crypto](#Crypto) -- [Auth](#Auth) -- [Oracle](#Oracle) -- [AENS](#AENS) -- [Contract](#Contract) -- [Call](#Call) -- [Chain](#Chain) +- [Bits](#bits) +- [Bytes](#bytes) +- [Char](#char) +- [Int](#int) +- [Map](#map) +- [Address](#address) +- [Crypto](#crypto) +- [Auth](#auth) +- [Oracle](#oracle) +- [AENS](#aens) +- [Contract](#contract) +- [Call](#call) +- [Chain](#chain) The following ones need to be included as regular files with `.aes` suffix, for example ``` include "List.aes" ``` -- [List](#List) -- [Option](#Option) -- [String](#String) -- [Func](#Func) -- [Pair](#Pair) -- [Triple](#Triple) -- [BLS12_381](#BLS12_381) -- [Frac](#Frac) +- [List](#list) +- [Option](#option) +- [String](#string) +- [Func](#func) +- [Pair](#pair) +- [Triple](#triple) +- [BLS12_381](bls12_381) +- [Frac](#frac) - [Set](#set-stdlib) -# Builtin namespaces +## Builtin namespaces They are available without any explicit includes. -## Bits +### Bits #### none ``` @@ -119,7 +119,7 @@ Bits.difference(a : bits, b : bits) : bits Each bit is true if and only if it was 1 in `a` and 0 in `b` -## Bytes +### Bytes #### to_int ``` @@ -153,7 +153,7 @@ Bytes.split(a : bytes(m + n)) : bytes(m) * bytes(n) Splits a byte array at given index -## Char +### Char #### to_int ``` @@ -172,7 +172,7 @@ Char.from_int(i : int) : option(char) Opposite of [to_int](#to_int). Returns `None` if the integer doesn't correspond to a single (normalized) codepoint. -## Int +### Int #### to_str ``` @@ -182,7 +182,7 @@ Int.to_str : int => string Casts integer to string using decimal representation -## Map +### Map #### lookup `Map.lookup(k : 'k, m : map('k, 'v)) : option('v)` @@ -229,7 +229,7 @@ Turns a list of pairs of form `(key, value)` into a map -## Address +### Address #### to_str ``` @@ -271,7 +271,7 @@ Address.to_contract(a : address) : C Cast address to contract type C (where `C` is a contract) -## Crypto +### Crypto #### sha3 ``` @@ -328,7 +328,7 @@ Crypto.verify_sig_secp256k1(msg : hash, pubkey : bytes(64), sig : bytes(64)) : b -## Auth +### Auth #### tx @@ -368,7 +368,7 @@ Auth.tx_hash : option(hash) Gets the transaction hash during authentication. -## Oracle +### Oracle #### register ``` @@ -380,7 +380,7 @@ Registers new oracle answering questions of type `'a` with answers of type `'b`. * The `acct` is the address of the oracle to register (can be the same as the contract). * `signature` is a signature proving that the contract is allowed to register the account - the `network id` + `account address` + `contract address` (concatenated as byte arrays) is - [signed](./sophia.md#delegation-signature) with the + [signed](./sophia_features.md#delegation-signature) with the private key of the account, proving you have the private key of the oracle to be. If the address is the same as the contract `sign` is ignored and can be left out entirely. * The `qfee` is the minimum query fee to be paid by a user when asking a question of the oracle. @@ -413,7 +413,7 @@ Responds to the question `q` on `o`. Unless the contract address is the same as the oracle address the `signature` (which is an optional, named argument) needs to be provided. Proving that we have the private key of the oracle by -[signing](./sophia.md#delegation-signature) +[signing](./sophia_features.md#delegation-signature) the `network id` + `oracle query id` + `contract address` @@ -481,25 +481,25 @@ Oracle.check_query(o : oracle('a, 'b), q : oracle_query('a, 'b)) : bool It returns `true` iff the oracle query exist and has the expected type. -## AENS +### AENS -The following functionality is available for interacting with the Aeternity -Naming System (AENS). +The following functionality is available for interacting with the æternity +naming system (AENS). If `owner` is equal to `Contract.address` the signature `signature` is ignored, and can be left out since it is a named argument. Otherwise we need a signature to prove that we are allowed to do AENS operations on behalf of `owner`. The [signature is tied to a network id](https://github.com/aeternity/protocol/blob/iris/consensus/consensus.md#transaction-signature), i.e. the signature material should be prefixed by the network id. -### Types +#### Types -#### name +##### name ``` datatype name = Name(address, Chain.ttl, map(string, AENS.pointee)) ``` -#### pointee +##### pointee ``` datatype pointee = AccountPt(address) | OraclePt(address) @@ -507,9 +507,9 @@ datatype pointee = AccountPt(address) | OraclePt(address) ``` -### Functions +#### Functions -#### resolve +##### resolve ``` AENS.resolve(name : string, key : string) : option('a) ``` @@ -520,7 +520,7 @@ associated with this name (for instance `"account_pubkey"`). The return type type checked against this type at run time. -#### lookup +##### lookup ``` AENS.lookup(name : string) : option(AENS.name) ``` @@ -534,53 +534,53 @@ let Some(Name(owner, FixedTTL(expiry), ptrs)) = AENS.lookup("example.chain") ``` -#### preclaim +##### preclaim ``` AENS.preclaim(owner : address, commitment_hash : hash, ) : unit ``` -The [signature](./sophia.md#delegation-signature) should be over +The [signature](./sophia_features.md#delegation-signature) should be over `network id` + `owner address` + `Contract.address` (concatenated as byte arrays). -#### claim +##### claim ``` AENS.claim(owner : address, name : string, salt : int, name_fee : int, ) : unit ``` -The [signature](./sophia.md#delegation-signature) should be over +The [signature](./sophia_features.md#delegation-signature) should be over `network id` + `owner address` + `name_hash` + `Contract.address` (concatenated as byte arrays) using the private key of the `owner` account for signing. -#### transfer +##### transfer ``` AENS.transfer(owner : address, new_owner : address, name : string, ) : unit ``` Transfers name to the new owner. -The [signature](./sophia.md#delegation-signature) should be over +The [signature](./sophia_features.md#delegation-signature) should be over `network id` + `owner address` + `name_hash` + `Contract.address` (concatenated as byte arrays) using the private key of the `owner` account for signing. -#### revoke +##### revoke ``` AENS.revoke(owner : address, name : string, ) : unit ``` Revokes the name to extend the ownership time. -The [signature](./sophia.md#delegation-signature) should be over +The [signature](./sophia_features.md#delegation-signature) should be over `network id` + `owner address` + `name_hash` + `Contract.address` (concatenated as byte arrays) using the private key of the `owner` account for signing. -#### update +##### update ``` AENS.update(owner : address, name : string, expiry : option(Chain.ttl), client_ttl : option(int), new_ptrs : map(string, AENS.pointee), ) : unit @@ -591,7 +591,7 @@ will not be updated, for example if `None` is passed as `expiry` the expiry block of the name is not changed. -## Contract +### Contract Values related to the current contract @@ -619,7 +619,7 @@ Contract.balance : int Amount of coins in the contract account -## Call +### Call Values related to the call to the current contract @@ -670,13 +670,13 @@ Call.gas_left() : int The amount of gas left for the current call. -## Chain +### Chain Values and functions related to the chain itself and other entities that live on it. -### Types +#### Types -#### tx +##### tx ``` record tx = { paying_for : option(Chain.paying_for_tx) , ga_metas : list(Chain.ga_meta_tx) @@ -686,18 +686,18 @@ record tx = { paying_for : option(Chain.paying_for_tx) , tx : Chain.base_tx } ``` -#### ga_meta_tx +##### ga_meta_tx ``` datatype ga_meta_tx = GAMetaTx(address, int) ``` -#### paying_for_tx +##### paying_for_tx ``` datatype paying_for_tx = PayingForTx(address, int) ``` -#### base_tx +##### base_tx ``` datatype base_tx = SpendTx(address, int, string) | OracleRegisterTx | OracleQueryTx | OracleResponseTx | OracleExtendTx @@ -711,9 +711,9 @@ datatype base_tx = SpendTx(address, int, string) ``` -### Functions +#### Functions -#### balance +##### balance ``` Chain.balance(a : address) : int ``` @@ -721,7 +721,7 @@ Chain.balance(a : address) : int The balance of account `a`. -#### block_hash +##### block_hash ``` Chain.block_hash(h : int) : option(bytes(32)) ``` @@ -734,7 +734,7 @@ allowed height. From FATE VM version 2 (IRIS) it will return the block hash of the current generation. -#### block_height +##### block_height ``` Chain.block_height : int" ``` @@ -742,7 +742,7 @@ Chain.block_height : int" The height of the current block (i.e. the block in which the current call will be included). -#### coinbase +##### coinbase ``` Chain.coinbase : address ``` @@ -750,7 +750,7 @@ Chain.coinbase : address The address of the account that mined the current block. -#### timestamp +##### timestamp ``` Chain.timestamp : int ``` @@ -758,7 +758,7 @@ Chain.timestamp : int The timestamp of the current block. -#### difficulty +##### difficulty ``` Chain.difficulty : int ``` @@ -766,7 +766,7 @@ Chain.difficulty : int The difficulty of the current block. -#### gas +##### gas ``` Chain.gas_limit : int ``` @@ -774,7 +774,7 @@ Chain.gas_limit : int The gas limit of the current block. -#### bytecode_hash +##### bytecode_hash ``` Chain.bytecode_hash : 'c => option(hash) ``` @@ -785,7 +785,7 @@ instantiated with a contract. The charged gas increases linearly to the size of the serialized bytecode of the deployed contract. -#### create +##### create ``` Chain.create(value : int, ...) => 'c ``` @@ -837,7 +837,7 @@ main contract Market = The typechecker must be certain about the created contract's type, so it is worth writing it explicitly as shown in the example. -#### clone +##### clone ``` Chain.clone : ( ref : 'c, gas : int, value : int, protected : bool, ... ) => if(protected) option('c) else 'c @@ -895,18 +895,18 @@ implementation of the `init` function does not actually return `state`, but calls `put` instead. Moreover, FATE prevents even handcrafted calls to `init`. -#### event +##### event ``` Chain.event(e : event) : unit ``` Emits the event. To use this function one needs to define the `event` type as a `datatype` in the contract. -# Includable namespaces +## Includable namespaces These need to be explicitly included (with `.aes` suffix) -## List +### List This module contains common operations on lists like constructing, querying, traversing etc. @@ -1253,7 +1253,7 @@ List.enumerate(l : list('a)) : list(int * 'a) Equivalent to [zip](#zip) with `[0..length(l)]`, but slightly faster. -## Option +### Option Common operations on `option` types and lists of `option`s. @@ -1438,7 +1438,7 @@ Option.choose_first(l : list(option('a))) : option('a) Same as [choose](#choose), but chooses from a list insted of two arguments. -## String +### String Operations on the `string` type. A `string` is a UTF-8 encoded byte array. @@ -1554,7 +1554,7 @@ blake2b(s : string) : hash Computes the Blake2B hash of the string. -## Func +### Func Functional combinators. @@ -1684,7 +1684,7 @@ Func.untuplify3(f : 'a * 'b * 'c => 'd) : ('a, 'b, 'c) => 'd Opposite to [tuplify](#tuplify). -## Pair +### Pair Common operations on 2-tuples. @@ -1736,7 +1736,7 @@ Pair.swap(t : ('a * 'b)) : ('b * 'a) Swaps elements. -## Triple +### Triple #### fst ``` @@ -1817,234 +1817,234 @@ Triple.rotl(t : ('a * 'b * 'c)) : ('b * 'c * 'a) Cyclic rotation of the elements to the left. -## BLS12\_381 +### BLS12\_381 -### Types +#### Types -#### fp +##### fp Built-in (Montgomery) integer representation 32 bytes -#### fr +##### fr Built-in (Montgomery) integer representation 48 bytes -#### fp2 +##### fp2 ``` record fp2 = { x1 : fp, x2 : fp }` ``` -#### g1 +##### g1 ``` record g1 = { x : fp, y : fp, z : fp } ``` -#### g2 +##### g2 ``` record g2 = { x : fp2, y : fp2, z : fp2 } ``` -#### gt +##### gt ``` record gt = { x1 : fp, x2 : fp, x3 : fp, x4 : fp, x5 : fp, x6 : fp, x7 : fp, x8 : fp, x9 : fp, x10 : fp, x11 : fp, x12 : fp } ``` -### Functions +#### Functions -#### pairing\_check +##### pairing\_check ``` BLS12_381.pairing_check(xs : list(g1), ys : list(g2)) : bool ``` Pairing check of a list of points, `xs` and `ys` should be of equal length. -#### int_to_fr +##### int_to_fr ``` BLS12_381.int_to_fr(x : int) : fr ``` Convert an integer to an `fr` - a 32 bytes internal (Montgomery) integer representation. -#### int_to_fp +##### int_to_fp ``` BLS12_381.int_to_fp(x : int) : fp ``` Convert an integer to an `fp` - a 48 bytes internal (Montgomery) integer representation. -#### fr_to_int +##### fr_to_int ``` BLS12_381.fr_to_int(x : fr) : int ``` Convert a `fr` value into an integer. -#### fp_to_int +##### fp_to_int ``` BLS12_381.fp_to_int(x : fp) : int ``` Convert a `fp` value into an integer. -#### mk_g1 +##### mk_g1 ``` BLS12_381.mk_g1(x : int, y : int, z : int) : g1 ``` Construct a `g1` point from three integers. -#### mk_g2 +##### mk_g2 ``` BLS12_381.mk_g2(x1 : int, x2 : int, y1 : int, y2 : int, z1 : int, z2 : int) : g2 ``` Construct a `g2` point from six integers. -#### g1_neg +##### g1_neg ``` BLS12_381.g1_neg(p : g1) : g1 ``` Negate a `g1` value. -#### g1_norm +##### g1_norm ``` BLS12_381.g1_norm(p : g1) : g1 ``` Normalize a `g1` value. -#### g1_valid +##### g1_valid ``` BLS12_381.g1_valid(p : g1) : bool ``` Check that a `g1` value is a group member. -#### g1_is_zero +##### g1_is_zero ``` BLS12_381.g1_is_zero(p : g1) : bool ``` Check if a `g1` value corresponds to the zero value of the group. -#### g1_add +##### g1_add ``` BLS12_381.g1_add(p : g1, q : g1) : g1 ``` Add two `g1` values. -#### g1_mul +##### g1_mul ``` BLS12_381.g1_mul(k : fr, p : g1) : g1 ``` Scalar multiplication for `g1`. -#### g2_neg +##### g2_neg ``` BLS12_381.g2_neg(p : g2) : g2 ``` Negate a `g2` value. -#### g2_norm +##### g2_norm ``` BLS12_381.g2_norm(p : g2) : g2 ``` Normalize a `g2` value. -#### g2_valid +##### g2_valid ``` BLS12_381.g2_valid(p : g2) : bool ``` Check that a `g2` value is a group member. -#### g2_is_zero +##### g2_is_zero ``` BLS12_381.g2_is_zero(p : g2) : bool ``` Check if a `g2` value corresponds to the zero value of the group. -#### g2_add +##### g2_add ``` BLS12_381.g2_add(p : g2, q : g2) : g2 ``` Add two `g2` values. -#### g2_mul +##### g2_mul ``` BLS12_381.g2_mul(k : fr, p : g2) : g2 ``` Scalar multiplication for `g2`. -#### gt_inv +##### gt_inv ``` BLS12_381.gt_inv(p : gt) : gt ``` Invert a `gt` value. -#### gt_add +##### gt_add ``` BLS12_381.gt_add(p : gt, q : gt) : gt ``` Add two `gt` values. -#### gt_mul +##### gt_mul ``` BLS12_381.gt_mul(p : gt, q : gt) : gt ``` Multiply two `gt` values. -#### gt_pow +##### gt_pow ``` BLS12_381.gt_pow(p : gt, k : fr) : gt ``` Calculate exponentiation `p ^ k`. -#### gt_is_one +##### gt_is_one ``` BLS12_381.gt_is_one(p : gt) : bool ``` Compare a `gt` value to the unit value of the Gt group. -#### pairing +##### pairing ``` BLS12_381.pairing(p : g1, q : g2) : gt ``` Compute the pairing of a `g1` value and a `g2` value. -#### miller_loop +##### miller_loop ``` BLS12_381.miller_loop(p : g1, q : g2) : gt ``` Do the Miller loop stage of pairing for `g1` and `g2`. -#### final_exp +##### final_exp ``` BLS12_381.final_exp(p : gt) : gt ``` Perform the final exponentiation step of pairing for a `gt` value. -## Frac +### Frac This namespace provides operations on rational numbers. A rational number is represented as a fraction of two integers which are stored internally in the `frac` datatype. @@ -2068,9 +2068,9 @@ language provides checkers to prevent unintended usage of them. Therefore the ty **will** allow that and the results of such comparison will be unspecified. You should use [lt](#lt), [geq](#geq), [eq](#eq) etc instead. -### Types +#### Types -#### frac +##### frac ``` datatype frac = Pos(int, int) | Zero | Neg(int, int) ``` @@ -2079,194 +2079,194 @@ Internal representation of fractional numbers. First integer encodes the numerat both must be always positive, as the sign is being handled by the choice of the constructor. -### Functions +#### Functions -#### make_frac +##### make_frac `Frac.make_frac(n : int, d : int) : frac` Creates a fraction out of numerator and denominator. Automatically normalizes, so `make_frac(2, 4)` and `make_frac(1, 2)` will yield same results. -#### num +##### num `Frac.num(f : frac) : int` Returns the numerator of a fraction. -#### den +##### den `Frac.den(f : frac) : int` Returns the denominator of a fraction. -#### to_pair +##### to_pair `Frac.to_pair(f : frac) : int * int` Turns a fraction into a pair of numerator and denominator. -#### sign +##### sign `Frac.sign(f : frac) : int` Returns the signum of a fraction, -1, 0, 1 if negative, zero, positive respectively. -#### to_str +##### to_str `Frac.to_str(f : frac) : string` Conversion to string. Does not display division by 1 or denominator if equals zero. -#### simplify +##### simplify `Frac.simplify(f : frac) : frac` Reduces fraction to normal form if for some reason it is not in it. -#### eq +##### eq `Frac.eq(a : frac, b : frac) : bool` Checks if `a` is equal to `b`. -#### neq +##### neq `Frac.neq(a : frac, b : frac) : bool` Checks if `a` is not equal to `b`. -#### geq +##### geq `Frac.geq(a : frac, b : frac) : bool` Checks if `a` is greater or equal to `b`. -#### leq +##### leq `Frac.leq(a : frac, b : frac) : bool` Checks if `a` is lesser or equal to `b`. -#### gt +##### gt `Frac.gt(a : frac, b : frac) : bool` Checks if `a` is greater than `b`. -#### lt +##### lt `Frac.lt(a : frac, b : frac) : bool` Checks if `a` is lesser than `b`. -#### min +##### min `Frac.min(a : frac, b : frac) : frac` Chooses lesser of the two fractions. -#### max +##### max `Frac.max(a : frac, b : frac) : frac` Chooses greater of the two fractions. -#### abs +##### abs `Frac.abs(f : frac) : frac` Absolute value. -#### from_int +##### from_int `Frac.from_int(n : int) : frac` From integer conversion. Effectively `make_frac(n, 1)`. -#### floor +##### floor `Frac.floor(f : frac) : int` Rounds a fraction to the nearest lesser or equal integer. -#### ceil +##### ceil `Frac.ceil(f : frac) : int` Rounds a fraction to the nearest greater or equal integer. -#### round_to_zero +##### round_to_zero `Frac.round_to_zero(f : frac) : int` Rounds a fraction towards zero. Effectively `ceil` if lesser than zero and `floor` if greater. -#### round_from_zero +##### round_from_zero `Frac.round_from_zero(f : frac) : int` Rounds a fraction from zero. Effectively `ceil` if greater than zero and `floor` if lesser. -#### round +##### round `Frac.round(f : frac) : int` Rounds a fraction to a nearest integer. If two integers are in the same distance it will choose the even one. -#### add +##### add `Frac.add(a : frac, b : frac) : frac` Sum of the fractions. -#### neg +##### neg `Frac.neg(a : frac) : frac` Negation of the fraction. -#### sub +##### sub `Frac.sub(a : frac, b : frac) : frac` Subtraction of two fractions. -#### inv +##### inv `Frac.inv(a : frac) : frac` Inverts a fraction. Throws error if `a` is zero. -#### mul +##### mul `Frac.mul(a : frac, b : frac) : frac` Multiplication of two fractions. -#### div +##### div `Frac.div(a : frac, b : frac) : frac` Division of two fractions. -#### int_exp +##### int_exp `Frac.int_exp(b : frac, e : int) : frac` Takes `b` to the power of `e`. The exponent can be a negative value. -#### optimize +##### optimize `Frac.optimize(f : frac, loss : frac) : frac` Shrink the internal size of a fraction as much as possible by approximating it to the point where the error would exceed the `loss` value. -#### is_sane +##### is_sane `Frac.is_sane(f : frac) : bool` For debugging. If it ever returns false in a code that doesn't call `frac` constructors or diff --git a/docs/sophia_syntax.md b/docs/sophia_syntax.md new file mode 100644 index 0000000..db438b5 --- /dev/null +++ b/docs/sophia_syntax.md @@ -0,0 +1,263 @@ +# Syntax + +## Lexical syntax + +### Comments + +Single line comments start with `//` and block comments are enclosed in `/*` +and `*/` and can be nested. + +### Keywords + +``` +contract elif else entrypoint false function if import include let mod namespace +private payable stateful switch true type record datatype main interface +``` + +### Tokens + +- `Id = [a-z_][A-Za-z0-9_']*` identifiers start with a lower case letter. +- `Con = [A-Z][A-Za-z0-9_']*` constructors start with an upper case letter. +- `QId = (Con\.)+Id` qualified identifiers (e.g. `Map.member`) +- `QCon = (Con\.)+Con` qualified constructor +- `TVar = 'Id` type variable (e.g `'a`, `'b`) +- `Int = [0-9]+(_[0-9]+)*|0x[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*` integer literal with optional `_` separators +- `Bytes = #[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*` byte array literal with optional `_` separators +- `String` string literal enclosed in `"` with escape character `\` +- `Char` character literal enclosed in `'` with escape character `\` +- `AccountAddress` base58-encoded 32 byte account pubkey with `ak_` prefix +- `ContractAddress` base58-encoded 32 byte contract address with `ct_` prefix +- `OracleAddress` base58-encoded 32 byte oracle address with `ok_` prefix +- `OracleQueryId` base58-encoded 32 byte oracle query id with `oq_` prefix + +Valid string escape codes are + +| Escape | ASCII | | +|---------------|-------------|---| +| `\b` | 8 | | +| `\t` | 9 | | +| `\n` | 10 | | +| `\v` | 11 | | +| `\f` | 12 | | +| `\r` | 13 | | +| `\e` | 27 | | +| `\xHexDigits` | *HexDigits* | | + + +See the [identifier encoding scheme](https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md) for the +details on the base58 literals. + +## Layout blocks + +Sophia uses Python-style layout rules to group declarations and statements. A +layout block with more than one element must start on a separate line and be +indented more than the currently enclosing layout block. Blocks with a single +element can be written on the same line as the previous token. + +Each element of the block must share the same indentation and no part of an +element may be indented less than the indentation of the block. For instance + +```sophia +contract Layout = + function foo() = 0 // no layout + function bar() = // layout block starts on next line + let x = foo() // indented more than 2 spaces + x + + 1 // the '+' is indented more than the 'x' +``` + +## Notation + +In describing the syntax below, we use the following conventions: + +- Upper-case identifiers denote non-terminals (like `Expr`) or terminals with + some associated value (like `Id`). +- Keywords and symbols are enclosed in single quotes: `'let'` or `'='`. +- Choices are separated by vertical bars: `|`. +- Optional elements are enclosed in `[` square brackets `]`. +- `(` Parentheses `)` are used for grouping. +- Zero or more repetitions are denoted by a postfix `*`, and one or more + repetitions by a `+`. +- `Block(X)` denotes a layout block of `X`s. +- `Sep(X, S)` is short for `[X (S X)*]`, i.e. a possibly empty sequence of `X`s + separated by `S`s. +- `Sep1(X, S)` is short for `X (S X)*`, i.e. same as `Sep`, but must not be empty. + + +## Declarations + +A Sophia file consists of a sequence of *declarations* in a layout block. + +```c +File ::= Block(TopDecl) + +TopDecl ::= ['payable'] 'contract' Con '=' Block(Decl) + | 'namespace' Con '=' Block(Decl) + | '@compiler' PragmaOp Version + | 'include' String + +Decl ::= 'type' Id ['(' TVar* ')'] '=' TypeAlias + | 'record' Id ['(' TVar* ')'] '=' RecordType + | 'datatype' Id ['(' TVar* ')'] '=' DataType + | (EModifier* 'entrypoint' | FModifier* 'function') Block(FunDecl) + +FunDecl ::= Id ':' Type // Type signature + | Id Args [':' Type] '=' Block(Stmt) // Definition + +PragmaOp ::= '<' | '=<' | '==' | '>=' | '>' +Version ::= Sep1(Int, '.') + +EModifier ::= 'payable' | 'stateful' +FModifier ::= 'stateful' | 'private' + +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 +``` + +There are three forms of type declarations: type aliases (declared with the +`type` keyword), record type definitions (`record`) and data type definitions +(`datatype`): + +```c +TypeAlias ::= Type +RecordType ::= '{' Sep(FieldType, ',') '}' +DataType ::= Sep1(ConDecl, '|') + +FieldType ::= Id ':' Type +ConDecl ::= Con ['(' Sep1(Type, ',') ')'] +``` + +For example, +```sophia +record point('a) = {x : 'a, y : 'a} +datatype shape('a) = Circle(point('a), 'a) | Rect(point('a), point('a)) +type int_shape = shape(int) +``` + +## Types + +```c +Type ::= Domain '=>' Type // Function type + | Type '(' Sep(Type, ',') ')' // Type application + | '(' Type ')' // Parens + | 'unit' | Sep(Type, '*') // Tuples + | Id | QId | TVar + +Domain ::= Type // Single argument + | '(' Sep(Type, ',') ')' // Multiple arguments +``` + +The function type arrow associates to the right. + +Example, +```sophia +'a => list('a) => (int * list('a)) +``` + +## Statements + +Function bodies are blocks of *statements*, where a statement is one of the following + +```c +Stmt ::= 'switch' '(' Expr ')' Block(Case) + | 'if' '(' Expr ')' Block(Stmt) + | 'elif' '(' Expr ')' Block(Stmt) + | 'else' Block(Stmt) + | 'let' LetDef + | Expr + +LetDef ::= Id Args [':' Type] '=' Block(Stmt) // Function definition + | Pattern '=' Block(Stmt) // Value definition + +Case ::= Pattern '=>' Block(Stmt) +Pattern ::= Expr +``` + +`if` statements can be followed by zero or more `elif` statements and an optional final `else` statement. For example, + +```sophia +let x : int = 4 +switch(f(x)) + None => 0 + Some(y) => + if(y > 10) + "too big" + elif(y < 3) + "too small" + else + "just right" +``` + +## Expressions + +```c +Expr ::= '(' LamArgs ')' '=>' Block(Stmt) // Anonymous function (x) => x + 1 + | 'if' '(' Expr ')' Expr 'else' Expr // If expression if(x < y) y else x + | Expr ':' Type // Type annotation 5 : int + | Expr BinOp Expr // Binary operator x + y + | UnOp Expr // Unary operator ! b + | Expr '(' Sep(Expr, ',') ')' // Application f(x, y) + | Expr '.' Id // Projection state.x + | Expr '[' Expr ']' // Map lookup map[key] + | Expr '{' Sep(FieldUpdate, ',') '}' // Record or map update r{ fld[key].x = y } + | '[' Sep(Expr, ',') ']' // List [1, 2, 3] + | '[' Expr '|' Sep(Generator, ',') ']' + // List comprehension [k | x <- [1], if (f(x)), let k = x+1] + | '[' Expr '..' Expr ']' // List range [1..n] + | '{' Sep(FieldUpdate, ',') '}' // Record or map value {x = 0, y = 1}, {[key] = val} + | '(' Expr ')' // Parens (1 + 2) * 3 + | Id | Con | QId | QCon // Identifiers x, None, Map.member, AELib.Token + | Int | Bytes | String | Char // Literals 123, 0xff, #00abc123, "foo", '%' + | AccountAddress | ContractAddress // Chain identifiers + | OracleAddress | OracleQueryId // Chain identifiers + +Generator ::= Pattern '<-' Expr // Generator + | 'if' '(' Expr ')' // Guard + | LetDef // Definition + +LamArgs ::= '(' Sep(LamArg, ',') ')' +LamArg ::= Id [':' Type] + +FieldUpdate ::= Path '=' Expr +Path ::= Id // Record field + | '[' Expr ']' // Map key + | Path '.' Id // Nested record field + | Path '[' Expr ']' // Nested map key + +BinOp ::= '||' | '&&' | '<' | '>' | '=<' | '>=' | '==' | '!=' + | '::' | '++' | '+' | '-' | '*' | '/' | 'mod' | '^' +UnOp ::= '-' | '!' +``` + +## Operators types + +| Operators | Type +| --- | --- +| `-` `+` `*` `/` `mod` `^` | arithmetic operators +| `!` `&&` `\|\|` | logical operators +| `==` `!=` `<` `>` `=<` `>=` | comparison operators +| `::` `++` | list operators + +## Operator precendences + +In order of highest to lowest precedence. + +| Operators | Associativity +| --- | --- +| `!` | right +| `^` | left +| `*` `/` `mod` | left +| `-` (unary) | right +| `+` `-` | left +| `::` `++` | right +| `<` `>` `=<` `>=` `==` `!=` | none +| `&&` | right +| `\|\|` | right