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 Ints.

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 a NoRef of type StkEl 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).