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
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
mathContract inside using
We can see in the first line of the code that it creates a variable starting from
the simplest expression, a constant:
zero <- new$ 0 int
Here this constant is used to create a new variable
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:
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
five <- new$ zero + 6 int
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 (
five) and constants (
storage += abs (param * five - 10 int)
You probably noticed that we used
storage here instead of
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
@ 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
storage_type is the only thing to fill and has to match the one from
the contract we are using this
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
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
We can now define a function that will compile
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
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
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 a
StkEl aon 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:
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
duup the element corresponding to this variable.
This logic is implemented in the
that exports the function to copy the value associated to a
Var to the top of
This function iterates over
StackVars to find the depth of the
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
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
then has to bring it all together in the resulting
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.Expr module you can also find
more useful constructors of
Expr and their compilation. Note the differences
Add constructor, that needs to contain type constraints.
This module also contains types and typeclasses to allow for better handling
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
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).