Expressions and operators¶
This chapter will introduce the concepts of expressions and operators in Indigo.
For a full list of the available operators and constructs, please refer to the relative reference page
Expression resolution¶
Indigo supports deep expressions resolution, meaning that you can build expressions starting from other expressions/operators. Behind the scenes it translates them to a series of Michelson instructions that operate on the stack to bring you the same result.
This chapter contains a Math.hs
module, let's take a look at it and proceed
with the explanation:
module Indigo.Tutorial.Expressions.Math
( mathContract
) where
import Indigo
mathContract :: IndigoContract Integer Integer
mathContract param = defContract do
zero <- new$ 0 int
_z <- new$ zero
five <- new$ zero + 6 int
five -= 1 int
storage += abs (param * five - 10 int)
storage :: HasStorage Integer => Var Integer
storage = storageVar
Remember: you can always bring it into the REPL by using :load
and then
print the mathContract
inside using printAsMichelson
or saveAsMichelson
.
We can see in the first line of the code that it creates a variable starting from
the simplest expression, a constant: 0 int
.
zero <- new$ 0 int
Here this constant is used to create a new variable zero
with new$
, all
variables are expressions too, so we can see in the next line how it creates
another variable from it:
_z <- new$ zero
Unused variable syntax
The second variable is also an example of an unused one, you can
see how its name starts with an underscore: _z
.
This denotes a variable that is not used later on in the code and it helps in keeping track of them, because if it didn't start with one you'd receive a warning.
You can see that indeed this is here just for demonstration purposes and it is not used anywhere else, in a real contract it would be better to just remove it.
If we proceed to the third line we have the allocation of five
, this is nothing
new: it sums (+
) the variable zero
and the constant 6 int
to make a new$
variable:
five <- new$ zero + 6 int
However 6
is not quite a good definition for five
, so in the next line we can
see that we use a compound operator -=
to update the value of five
instead of
computing the difference first (five - 1 int
) and then using =:
:
five -= 1 int
When possible, you should always try to use compound operators, because they are more efficient as they avoid unnecessary duplication on the stack.
In the next and final line, we can see all of this together, we update the
storage
by adding to it (+=
), we use a prefix operator (abs
), variables
(params
and five
) and constants (10 int
):
storage += abs (param * five - 10 int)
The storage
definition¶
You probably noticed that we used storage
here instead of storageVar
, and
that there is a weird storage
definition below our contract:
storage :: HasStorage Integer => Var Integer
storage = storageVar
We need this for the same reason we have to use ::
and @
sometimes in our
code, which is to remove ambiguity for the compiler.
Its definition is always the same, following the formula:
storage :: HasStorage <storage_type> => Var <storage_type>
storage = storageVar
where storage_type
is the only thing to fill and has to match the one from
the contract we are using this storage
in.
Although we now know how to compose operators, expressions, variables, etc. we still only have linear code execution, hence the next chapter: Imperative statements
Technical details: expressions evaluation and variables¶
With the strongly typed foundations described in the previous chapter we want to define expression manipulation and operations.
For this we need an assignment operator that can create a new variable from some
expression and to define an Expr
datatype for expressions construction.
The latter is contained in the Indigo.Common.Expr
module.
Here we take a look at a simplified version of it to explain it better; let's
suppose we defined it like this:
data Expr a where
C :: a -> Expr a
V :: Var a -> Expr a
Add :: Expr Int -> Expr Int -> Expr Int
The first constructor creates an expression from a constant, the second from a
variable and the last one is for the addition operation of two expressions of Int
s.
We can now define a function that will compile Expr
to IndigoM
code, that
computes the value of the given expression and leaves it on top of the stack:
compileExpr :: Expr a -> IndigoState inp (a : inp) ()
We can see that the resulting type will have a value of type a
on the top of
the stack as a result of the computation.
Let's define it step by step, explaining the definition for each constructor:
compileExpr (C a) = do
md <- iget
iput $ GenCode () (pushNoRefMd md) (L.push a) L.drop
As a reminder: here we use the do
syntax that thanks to RebindableSyntax
resolves to IndigoState
.
In the first line we get the current state, of type MetaData
, while in the second
line we construct a GenCode
datatype, which is the resulting type.
GenCode
consists (in this order) of a return value, the updated MetaData
,
the generated Lorentz code and the "cleaning" Lorentz code.
As you can see:
- we have no return value just yet
- the metadata is updated by
pushNoRefMd
, which adds aNoRef
of typeStkEl a
on the top of the stack to satisfy the output stack type:a : inp
- the Lorentz code is just a
PUSH
, that puts the value on the stack - the "cleaning" code is just a
DROP
, this will come in handy if we ever need to remove what we just added (e.g. while exiting a scope or context)
The next step is the constructor for variables:
compileExpr (V a) = do
md@(MetaData s _) <- iget
iput $ GenCode () (pushNoRefMd md) (varActionGet a s) L.drop
This may seem similar to the previous one, but that is because the complexity is
hidden by the only difference: varActionGet
.
As stated previously, the main idea here is that we assign an index to each variable
and link them to an element of the stack that it refers to.
This is where things come together.
To compute an expression that is just a variable we have to generate the Lorentz
code to duup
the element corresponding to this variable.
This logic is implemented in the Indigo.Backend.Lookup
module,
that exports the function to copy the value associated to a Var
to the top of
the stack: varActionGet
.
This function iterates over StackVars
to find the depth of the Var
, then
check all the required constraints to use the duupX
Lorentz macro.
You can notice that the same module contains not only the getter of a variable,
but also the varActionSet
setter (to assign to the variable the value on the
top of the stack) and the varActionUpdate
updater (to update the variable using
a binary instruction, the value at the top of the stack and the current value of
the variable itself)
The last step for compileExpr
is the Add
constructor:
compileExpr (Add e1 e2) = IndigoState $ \md ->
let GenCode _ md2 cd2 _cl2 = runIndigoState (compileExpr e2) md in
let GenCode _ _md1 cd1 _cl1 = runIndigoState (compileExpr e1) md2 in
GenCode () (pushNoRefMd md) (cd2 # cd1 # L.add) L.drop
This constructor needs to run the compileExpr
for the operands of Add
, it
then has to bring it all together in the resulting GenCode
.
In particular, it has to use subsequent metadatas for the operation, but ignore
them in the end (because what we put on the stack will be consumed and a new
element inserted), compose the generated Lorentz code and ignore the return value
as well as the cleaning code (because this too needs to be mindful of the same
changes to the stack as metadata).
Now we can finally write the assignment operator, to create a variable referring to the top of the stack after a computed expression takes place. This can be done with a function having the type
makeTopVar :: KnownValue x => IndigoState (x : inp) (x : inp) (Var x)
You can find its implementation in the Indigo.Common.State
module.
In the Indigo.Common.Expr
module you can also find
more useful constructors of Expr
and their compilation. Note the differences
in the Add
constructor, that needs to contain type constraints.
This module also contains types and typeclasses to allow for better handling
(see IsExpr
for the ability to derive expressions from types).
Technical details: a note about HasStorage¶
HasStorage
is one of the specific parts of IndigoContract
that separates it
from any generic IndigoM
.
It is such because every contract has a storage, and we can make it available during compilation.
You can find out more starting by looking at compileIndigoContract
in the
Indigo.Compilation
module, which is the function that compiles an Indigo contract
to Lorentz (code that in turn can be compiled to Morley and Michelson).