Love Reference¶
Contract Format¶
All the contracts have the following form:
#love-1.0
use C = KT1...
type storage = TYPEDEF
val v : TYPE = BODY
val%private x : TYPE = BODY
val f
(x : TYPE) : TYPE =
BODY
val g TVARS
(x : TYPE) : TYPE =
BODY
val%init (parameter : TYPE) =
BODY
val%view check storage (parameter : TYPE) : TYPE =
BODY
val%entry entrypoint1
s1
d1
(parameter : TYPE) =
BODY
val%entry entrypoint2
s2
d2
(p2 : TYPE) =
BODY
val%entry default
s3
d3
(p3 : TYPE) =
BODY
...
The version
statement (#love-1.0) tells the Love version
in which the contract is written. The compiler will reject any
contract that an incompatible version (too old or recent). By default,
a contract with #love
will be interpreted with the latest Love version.
- A contract is composed of different elements.
- External dependencies (
use C = KT1...
) - Type declarations (
type t = TYPEDEF
) - Local values and functions (
val f, val%private x
) - At most one initializer (
val%init
) - Views (
val%view
) - Entrypoints (
val%entry
)
- External dependencies (
Overview¶
Each value defined by a contract is declared with the keyword val
.
Private values are values that can be used inside the contract, but that are invisible
by others – they are declared with the keyword val%private
.
Values does not have access to the storage. In order to read the storage,
views can be defined with the keyword val%view
.
Views are defined as functions with an argument of type storage
and several parameter.
As for usual values, the return type of the view must be specified.
Each entry point is a special function declared with the keyword val%entry
.
An entry point must have three arguments in a specific order : the storage, the transaction amount
activating the entry point (of type dun
) and the parameter.
The last argument must be annotated by its type, which can be different from one entry point
to another.
If there is an entry point named default
, it will be the default
entry point for the contract, i.e. the one that is called when the
entry point is not specified in Contract.call
.
An entry point always returns a pair (operations, storage)
, where
operations
is a list of internal operations to perform after
execution of the contract, and storage
will replace the storage of the
contract after the call. The type of the pair must match the type of a
pair where the first component is a list of opertations and the second
is the type of the storage argument.
Every value/entrypoint/subcontract must be given a unique name within the same contract.
Love contracts are meant to be inter-operable, they can be used by other contracts
on the network. The use
keyword allows to define namespaces for external contracts
given the address KT1...
.
Expressions¶
Variables¶
Variables are defined with the keyword let
and saved in the execution environment
with the keyword in
.
let x = 3 in let y = 4 in x + y (* The expression x + y equals 7. *) let result = ( let x = 3 in x + 3 ) in result - 1 (* The variable 'result' locally defines 'x' and uses it. After the definition of 'result', 'x' is forgotten. *)
Function are defined (similarly to variables) between the let - in
keywords
by adding the list of arguments with their types, separated by spaces, after the
function name.
let f (x : int) (y : int) = x + y in f 3 6
It is also possible to define anonymous functions with the keyword fun
.
(fun (x : int) (y : int) -> x + y) 5 7 (* This expression is equal to 12. *)
Basic Types and Values¶
Types define the data structures the contract can use.
Basic Types¶
The simple built-in types are:
unit
: whose only constructor is()
bool
: Booleansint
: Unbounded integersdun
: The type of amountsstring
: characters sequencesbytes
: bytes sequencestimestamp
: dates and timestampskey
: cryptographic keyskeyhash
: hashes of cryptographic keyssignature
: cryptographic signaturesoperation
: type of operations, can only be constructedaddress
: abstract type of contract addresses
Composite Types¶
Types can be composed using the following type operators:
- tuples: noted
t1 * t2
,t1 * t2 * t3
, etc. - functions:
t1 -> t2
is the type of functions fromt1
tot2
.
Custom Types¶
As in OCaml, there exist three kind of custom types in love : sum types, record types and aliases.
Sum types are defined by a list of constructors. Each constructor is associated to a list of types. A value of such a type is defined by a unique constructor associated to an association of values that matches the type of the constructor.
type t = A | B of int | C of t * int val a : t = A val b : t = B 42 val c : t = C (A, 5)
Record types are defined by a list of fields. Each field is associated to a list of types. A value of such a type is defined by the definition of each of its fields.
type r = { field1: int; field2: nat; field3: bool } val get_field1 (r : r) : int = r.field1 val set_field1_and_2 (r : r) (n : int) (field2 : nat) : r = {r with field1 = n; field2}
Aliases are shortcuts for other types.
type alias = int * bool val x : alias = (1, true)
It is also possible to define types that depend on other types. By prefixing variable types to the type name, it is possible to use them in their definition.
type 'a alias1 = ('a * bool) val x : bool alias1 = (true, false) type ('a, 'b) alias2 = ('a * 'b) val y : (bool, int) alias2 = (true, 2) type 'a option = None | Some of 'a type 'a record = { x : 'a; y : 'a }
Love defines several polymorphic combinators :
- lists:
'a list
is the type of lists of elements in'a
- sets:
'a set
is the type of sets of elements in'a
('a
must be a comparable type) - maps:
('key, 'val) map
is the type of maps whose keys are of type'key
, a comparable type, and values of type'val
; - big maps:
('key, 'val) big_map
is the type of lazily deserialized maps whose keys are of type'key
(a comparable type) and values of type'val
; - option:
'a option = None | Some of 'a
- variant:
('a, 'b) variant = Left of 'a | Right of 'b
- contracts:
S.instance
is the type of contracts (instances) of signatureS
(see Contract Types and Signatures);
and the polymorphic data type:
Quantified Types¶
In Love, polymorphic values have a specific type:
- polymorphic type: forall 'a. TYPE('a)
This is the type of functions depending on type arguments.
(* This function has type forall 'a. 'a -> 'a *) val identity 'a (x : 'a) : 'a = x (* By instanciating 'a by [:int], we define an integer specialized function *) val int_identity (x : int) : int = identity [:int] x
Such values can be functions as well as constants. For example, the empty list
[]
has the type forall 'a. 'a list
.
So as to add elements to a polymorphic collection, types must be instanciated
by providing a type argument [:TYPE]
. For example, [][:string]
is the empty list containing string
s.
Similarly, it is possible to define polymorphic values by adding type arguments to its definition. For example,
let pair 'a 'b = [][:'a], [][:'b]
defines a polymorphic value that, given two type arguments, returns a pair of lists. Its type isforall 'a, forall 'b, 'a list * 'b list
andpair[:int][:string]
has type int list * string list.
Constant Values¶
The unique constructor of type unit
is ()
.
The two Booleans (bool
) constants are:
true
false
int
: an unbounded integer, positive or negative, simply written0
,1
,2
,-1
,-2
, …dun
: an unbounded positive float of Duns, written with adn
suffix (1.00dn
)
Strings (string
) are delimited by the characters "
and "
.
Bytes (bytes
) are sequences of hexadecimal pairs preceeded by 0x
, for
instance:
0x
0xabcdef
Timestamps (timestamp
) are written in ISO 8601 format, like in Michelson:
2015-12-01T10:01:00+01:00
Keys, key hashes and signatures are base58-check encoded, the same as in Michelson:
dn1b5rSQesATpGWjy1yBPpjbTnmDB5VfBBso
is a key hash (keyhash
)edpkuit3FiCUhd6pmqf9ztUTdUs1isMTbF9RBGfwKk1ZrdTmeP9ypN
is a public key (key
)edsigedsigthTzJ8X7MPmNeEwybRAvdxS1pupqcM5Mk4uCuyZAe7uEk68YpuGDeViW8wSXMrCi5CwoNgqs8V2w8ayB5dMJzrYCHhD8C7
is a signature (signature
)
There are also three types of collections: lists, sets and maps.
As they are polymorphic, a type application [:TYPE]
must be provided
(see Polymorphism
for more details) to be created directly:
- Lists:
["x"; "y"] [:string]
for astring list
; - Sets:
{1; 2; 3; 4}[:int]
for anint set
; - Maps:
{{1 -> "x"; 2 -> "y"; 3 -> "z"}}[:int][:string]
for a(int, string) map
;
Note that the type argument is compulsory.
In the case of sum types, all types must be provided for every constructors.
For example, options constructors (from the option
type) can be defined with:
- The None case:
None [:TYPE]
- The Some case:
Some EXP [:TYPE]
The Modules and Contracts System¶
The system described in this section allows to define several contracts and modules in the same file, to reference contracts by their names, and to call contracts defined in other files.
use C = KT1C...
module M = struct
type t = int * bool
val%private x : t = (5, true)
val f (v : t) : int = v.0
end
contract Integer = struct
type storage = int
val zero : storage = 0
val succ (x : storage) : storage = x + 1
val%private prev (x : storage) : storage = x - 1
val%view get storage (param : unit) : int = storage
val%entry set storage duns (param : int) =
([][:operation], param)
val%init storage (i : int) = i
end
val create_c (k : keyhash) : (operation * address) =
let i = (contract Integer) in
Contract.create [:int] (Some k [:keyhash]) 0.0dn i 0
contract type CT = sig
type storage
val zero : storage -> storage
val%view get : unit -> storage
val%entry set : storage
end
contract UseC = struct
use C = KT1C...
(* use D = KT1D *) (* UseC cannot depend on D because it is not
a dependency of the main contract. *)
(* use E = KT1C*) (* While KT1C is a dependency of the main contract,
E is not its name. *)
end
val get_c (a : address) : (instance CT) option = Contract.at<!CT> a
val get_c2 : address -> (instance CT) option = fun (a : address) -> Contract.at<!CT> a
type 'a t = 'a * Integer.storage
val x : forall 'a. 'a -> 'a t = fun 'a (x : 'a) -> (x, 0)
module MyList =
struct
type rec 'a t =
Empty
| Cons of ('a * 'a t)
val empty 'a : 'a t = (Empty [:'a])
val singleton 'a (x : 'a) : 'a t = Cons (x, (Empty [:'a])) [:'a]
end
module Or =
struct
type ('a, 'b) t =
Left of 'a
| Right of 'b
val left 'a (x : 'a) 'b : ('a, 'b) t = (Left x [:'a] [:'b])
val right 'a 'b (x : 'b) : ('a, 'b) t = (Right x [:'a] [:'b])
end
The notion of contract and module structures in Love is a way
to define namespaces and to encapsulate types, values and contracts in
packages. These packages are called structures and are introduced with
the struct
keyword and closed with end
.
Modules, introduced with the keyword module
, can contain types and
values but cannot contain any entry points nor views.
Contracts are introduced with the keyword contract
, they can contain
types, values and may or may not have entry points and views. If
a type storage
is defined in a contract, it will represent the data
structure stored in the context of the contract. Otherwise, the storage
type is unit
by default.
Types in scope (defined before their use) can be referred to anywhere,
provided they are adequately qualified (with a dot .
notation).
Values are exported outside the module or the contract by default,
which means they can be used by other modules and contracts. One can
annotate the value with %private
to prevent exporting the value.
For instance the following example defines a module M
with a type
t
, a private value x
and an exported function f
. The function
f
can be called outside the module as M.f
, whereas x
cannot
(the compiler will complain that is does not know the symbol M.x
if we
try to use it elsewhere).
module M = struct
type t = int * bool
val%private x : t = (5, true)
val f (v : t) : int = v.0
end
The contract Integer
can be defined as such. It defines a type storage
The value zero
and the function succ
are exported and can be
called with Integer.zero
and Integer.succ
outside the contract,
whereas prev
cannot.
Functions get
and set
are respectively a view and an entrypoint of C
.
The type of Integer.get
is (unit, int) view
and the type of C.set
is
int entry_point
.
contract Integer = struct
type storage = int
val zero : storage = 0
val succ (x : storage) : storage = x + 1
val%private prev (x : storage) : storage = x - 1
val%view get storage (param : unit) : int = storage
val%entry set storage duns (param : int) =
([][:operation], param)
val%init storage (i : int) = i
end
As you see, Integer
does not depend on M
. That is because in Love, contracts
must be self-contained.
First Class Contract Structures¶
Contracts structures (note we are not talking about contract instances here) can also be used as first class values:
val create_c (k : keyhash) : (operation * address) =
let i = (contract Integer) in
Contract.create [:int] (Some k [:keyhash]) 0.0dn i 0
The operation generated by Contract.create
will originate a contract
with the code of Integer
.
Contract Types and Signatures¶
As for values, it is possible to specify the type of a contract with
contract signatures. Contract signatures are introduced with
the keyword sig
and defined with the keyword contract type
contract type CT = sig
type storage
val zero : storage -> storage
val%view get : unit -> storage
val%entry set : storage
end
A contract signature contains a declaration of the types with (or without) their definition, and a set of values, functions, views and entry points.
The type of a contract (instance) whose signature is CT
is written
instance CT
. Note that CT
must be declared as a contract signature
beforehand if we want to declare values of type instance CT
.
For example:
type t = {
counter : int;
dest : instance CT;
}
is a record type with a contract field dest
of signature CT
.
Predefined Contract Signatures¶
The contract signature UnitContract
is built-in, in Love, and
stands for empty contracts.
External dependencies¶
Some contracts may require to use functions and entry points from other contracts. In Love, there exists two ways to import external contracts.
When the required contract is already known, it can be imported with the keyword
use
. Its nameC
can be used in the rest of the contract. .. literalinclude:: tests/doc_contract.lov :lines: 1A subcontract can also depend on external contracts and are free to use the
use
keyword. However, the parent contract also must define the same dependencies (with the same module name), otherwise the compilation will fail. .. literalinclude:: tests/doc_contract.lov :lines: 36-42When the required contract address is only known at runtime, it can be imported with the primitive
Contract.at
and specifying its signature. .. literalinclude:: tests/doc_contract.lov :lines: 44NB : the signature
CT
defines a subset of public fields ofInteger
. It would have been possible to import a contract originated bycreate_c
with such a signature, making the functionsucc
invisible.
It is only possible to import Love contracts.
Anonymous Functions¶
Functions can be defined on-the-fly with the keyword fun
. The arguments syntax
remains the same.
val get_c (a : address) : (instance CT) option = Contract.at<!CT> a
val get_c2 : address -> (instance CT) option = fun (a : address) -> Contract.at<!CT> a
Polymorphism¶
Types in Love can be polymorphic, i.e. they may be parametrized by other types.
type 'a t = 'a * Integer.storage
The type 'a t
defines a polymorphic tuple depending on 'a
, and
the type dun t
is an alias for dun * int
.
Values and functions also can depend on type parameters.
Such an element is defined as a function, except the argument name
is not annotated with a type.
Its type is declared with the keyword forall
.
val x : forall 'a. 'a -> 'a t = fun 'a (x : 'a) -> (x, 0)
Sum types and record types can also be polymorphic.
module MyList =
struct
type rec 'a t =
Empty
| Cons of ('a * 'a t)
val empty 'a : 'a t = (Empty [:'a])
val singleton 'a (x : 'a) : 'a t = Cons (x, (Empty [:'a])) [:'a]
end
Note that in the case of Cons
, the constructor also required a
type application.
module Or =
struct
type ('a, 'b) t =
Left of 'a
| Right of 'b
val left 'a (x : 'a) 'b : ('a, 'b) t = (Left x [:'a] [:'b])
val right 'a 'b (x : 'b) : ('a, 'b) t = (Right x [:'a] [:'b])
end
Ghost code¶
Love contracts may contain parts of the code that is considered as ghost. In particular, users can define ghost functions:
val%ghost this_is_my_ghost_function (l : nat list) : bytes =
Bytes.pack [:nat list] l
and ghost views:
type storage = { a : int ; b : int }
val%ghostView this_is_my_ghost_view storage (_ : unit) : bool =
storage.b >[:int] storage.b
The default behavior of the node is to ignore these pieces of code when originating a smart contract containing them. However, the use of –keep-ghost option will cast ghost functions (resp. views) to normal functions (resp. views) and include them in originations.
Ghost code can be useful for online testing, or for offline tools/interpreters that allow to check conditions or invariants on a contract’s storage without weighing down the originated contract on-chain.
Comparability¶
Types 'a set
, ('a, 'b) map
and ('a, 'b) bigmap
are
special types requiring 'a
to be comparable.
All values are not comparable. Only two values of the following types can be compared with each other:
unit
bool
int
tez
string
bytes
timestamp
keyhash
address
Combination of types can also be comparable in certain cases.
'a * 'b
when'a
and'b
are comparable'a option
,``’a list``,'a set
when'a
is comparable('a, 'b) map
when'a
and'b
are comparable{field1 : 'a; field2 : 'b; ...}
when'a
,'b
, … are comparableA of 'a | B of 'b ...
when'a
,'b
, … are comparable
When defining a type with polymorphic arguments, it is possible to force some arguments to be comparable.
type (('a[Comparable]), 'b) t = 'a * 'b
type correct = (int, int) t
type incorrect = ((int -> int), int) t
Type t
define a tuple in which the first argument must be comparable.
As int
is comparable, the type correct
is well defined while incorrect
is not because int -> int
is not a comparable type.
Love Grammar¶
Toplevel:
#love
[: VERSION]?- ContractContent*
Contract:
struct
StructContent*end
Module:
* struct
ModContent* end
Signature:
* sig
SigContent* end
TypeName:
* LIDENT
* '
.LIDENT LIDENT
* (
['
.LIDENT ,
]+ '
.LIDENT )
LIDENT
ModContent:
type
TypeName=
Typetype
TypeName= {
[ LIDENT:
Type;
]+}
type
TypeName=
[|
UIDENTof
Type ]+module
UIDENT=
Modulecontract
UIDENT=
Contractcontract type
UIDENT=
Signatureval
LIDENT Arg*:
Type =`` Expression
ContractContent:
- ModStructure
val%view
LIDENT LIDENT(
Pattern:
Type)
:
Type =`` Expressionval%init
storage
(
Pattern:
Type)
=`` Expressionval%entry
LIDENT LIDENT LIDENT(
Pattern:
Type)
=
Expression
Arg:
* (
Pattern :
Type )
SigContent:
type
TypeName=
Typetype
TypeNameval%entry
LIDENT:
LIDENT:
Type->
LIDENT:
Type-> operation list *
Type
Expression:
- LIDENT
- UIDENT
.
LIDENT - [LIDENT
.
]+ LIDENT - [LIDENT
.
]+ LIDENT<-
Expression (
Expression:
Type)
if
Expressionthen
Expressionif
Expressionthen
Expressionelse
Expression(contract
UIDENT)
let
Pattern=
Expressionin
Expression- Expression
;
Expression - Expression Expression
match
Expressionwith | [] ->
Expression|
LIDENT::
LIDENT->
Expressionmatch
Expressionwith
[|
MatchPattern->
Expression ]*None
Some
Expression- Expression
::
Expression - Expression
[:
Type]
- Constant
Pattern:
- LIDENT
_
(
Pattern [,
Pattern]*)
- Pattern
as
LIDENT contract
UIDENT:
[Signature | UIDENT]
MatchPattern:
- Pattern
- UIDENT
- UIDENT Pattern
Type:
unit
bool
int
nat
dun
string
bytes
timestamp
key
keyhash
signature
operation
address
- Type
option
- Type
list
- Type
set
(
Type,
Type) map
(
Type,
Type) big_map
- Type [
*
Type]+ - Type
->
Type - Type? LIDENT
(
Type+)
LIDENT
Constant:
dn1
B58Char+(33)dn2
B58Char+(33)dn3
B58Char+(33)edpk
B58Char+(50)sppk
B58Char+(50)p2pk
B58Char+(50)edsig
B58Char+(94)p2sig
B58Char+(93)spsig1
B58Char+(93)KT1
B58Char+(33)0x
[HexChar HexChar]*true
false
- DIGIT [DIGIT |
_
]* - DIGIT [DIGIT |
_
]*p
- DIGIT [DIGIT |
_
]* [.
[DIGIT |_
]*]? [dn
|dun
|DUN
] - DAY [
T
HOUR [ TIMEZONE ]?]? "
CHAR*"
()
[
Constant+`;`]
Map
|Map
[
Constant+``;``]
Set
|Set
[
Constant+``;``]
BigMap
|BigMap
[
Constant+``;``]
fun
Pattern->
Expression
B58Char:
- [
1
-9
|A
-H
|J
-N
|P
-Z
|a
-k
|m
-z
]
HexChar:
- [
0
-9
|A
-F
|a
-f
]
LIDENT:
- [
a
-z
|_
] [A
-Z
|a
-z
|_
|'
|0
-9
]*
UIDENT:
- [
A
-Z
] [A
-Z
|a
-z
|_
|'
|0
-9
]*
DIGIT:
- [
0
-9
]
DAY:
- DIGIT+(4)
-
DIGIT+(2)-
DIGIT+(2)
HOUR:
- DIGIT+(2)
:
DIGIT+(2) [:
DIGIT+(2)]?
TIMEZONE:
+
DIGIT+(2):
DIGIT+(2)Z