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)

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 take two arguments : the current storage (of type storage) and a 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: Booleans
  • int: Unbounded integers
  • dun: The type of amounts
  • string: characters sequences
  • bytes: bytes sequences
  • timestamp: dates and timestamps
  • key: cryptographic keys
  • keyhash: hashes of cryptographic keys
  • signature: cryptographic signatures
  • operation: type of operations, can only be constructed
  • address: 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 from t1 to t2.

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 signature S (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 is forall 'a, forall 'b, 'a list * 'b list and pair[: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 written 0, 1, 2, -1, -2, …
  • dun : an unbounded positive float of Duns, written with a dn 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 a string list;
  • Sets: {1; 2; 3; 4}[:int] for an int 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 name C can be used in the rest of the contract. .. literalinclude:: tests/doc_contract.lov :lines: 1

    A 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-42

  • When 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: 44

    NB : the signature CT defines a subset of public fields of Integer. It would have been possible to import a contract originated by create_c with such a signature, making the function succ 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 comparable
  • A 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 = Type
  • type TypeName = { [ LIDENT : Type ;]+ }
  • type TypeName = [ | UIDENT of Type ]+
  • module UIDENT = Module
  • contract UIDENT = Contract
  • contract type UIDENT = Signature
  • val LIDENT Arg* : Type =`` Expression

ContractContent:

  • ModStructure
  • val%view LIDENT LIDENT ( Pattern : Type ) : Type =`` Expression
  • val%init storage ( Pattern : Type ) =`` Expression
  • val%entry LIDENT LIDENT LIDENT ( Pattern : Type ) = Expression

Arg: * ( Pattern : Type )

SigContent:

  • type TypeName = Type
  • type TypeName
  • val%entry LIDENT : LIDENT : Type -> LIDENT : Type -> operation list * Type

Expression:

  • LIDENT
  • UIDENT . LIDENT
  • [LIDENT .]+ LIDENT
  • [LIDENT .]+ LIDENT <- Expression
  • ( Expression : Type )
  • if Expression then Expression
  • if Expression then Expression else Expression
  • (contract UIDENT )
  • let Pattern = Expression in Expression
  • Expression ; Expression
  • Expression Expression
  • match Expression with | [] -> Expression | LIDENT :: LIDENT -> Expression
  • match Expression with [ | 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