# Chapter 12
# Union types and entrypoints
So far, in all the contracts we have written, the flow of the execution was pretty straighforward:
- The transaction is sent to the contract with a single parameter.
- The contract uses or not the parameter and runs its code.
- A pair containing a list of operations and the new storage is returned.
This works well for simple contracts, but what if you want your contract to do multiple things? You could write different separate contracts of course, but sharing their storage and state will be more complicated. In this chapter, you will learn how to change the behaviour of your contract according to the parameters it received by implementing entrypoints!
Let's check a very simple example and see how we can modify it to change its behaviour:
parameter int ;
storage int ;
code {
UNPAIR ;
ADD ;
NIL operation ;
PAIR ;
} ;
RUN %default 5 6 ;
stdout
parameter int; storage int; code { { DUP ; CAR ; DIP { CDR } } ; ADD ; NIL operation ; PAIR }; RUN: use %default; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
This couldn't be more simple 😅 The contract takes the int
sent in the parameters and adds it to the int
in the storage.
Now, imagine you want the same contract to add or subtract two numbers. There must be something in the parameter that tells the contract what to do. This is when union types
intervene! Let's see first how that would look like:
parameter (or int int) ;
storage int ;
code {
UNPAIR ;
IF_LEFT
{ ADD }
{ SUB } ;
NIL operation ;
PAIR ;
} ;
RUN %default (Left 5) 6 ;
stdout
parameter (or int int); storage int; code { { DUP ; CAR ; DIP { CDR } } ; IF_LEFT { ADD } { SUB } ; NIL operation ; PAIR }; RUN: use %default; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
You can now see that there are three different things in this new contract compared to the previous one:
- The parameter is
(or (int %increment) (int %decrement))
instead ofint
. - There is a conditional structure in the code with
IF_LEFT
. - The parameters of the
RUN
instruction are different.
In order to tell the contract to change its behaviour, we are using a union type
. As its name indicates, a union type
is a type made of the union of two other types. It holds one value that may be of either type. In its most basic form, a union type is declared between parentheses with the keyword or
followed by the two types you want to use. The type on the left is the left
part of the union, the type on the right the right
part. After implementing the union type as a parameter, we can pass two different arguments to the contract: a union value with a value on the left or a union value with a value on the right. Now, we want the contract to behave differently according to the value present in the union value. This is what IF_LEFT
does. When we unpair the pair of parameter/storage, the contract knows it expects a union type. If the left side holds a value, the value in (Left value)
will be pushed onto the stack. If the right side holds a value, the value in (Right value)
will be pushed. This is what you can see after the RUN
instruction, we indicated (Left 5)
as the value we want to use.
As you may have guessed, IF_LEFT
will branch into the first pair of curly braces if the left side of the union value holds a value or it will branch into the second pair of curly braces if the right side of the union value holds a value. After branching, the naked value inside the union value is pushed onto the stack and ready to be used. In the case of this example, the int
value is added to the value in the storage and the new storage is returned.
To drive the point home, let's see what happens if we want to subtract the value passed as a parameter:
parameter (or int int) ;
storage int ;
code {
UNPAIR ;
IF_LEFT
{ ADD }
{ SUB } ;
NIL operation ;
PAIR ;
} ;
RUN %default (Right 5) 6 ;
stdout
parameter (or int int); storage int; code { { DUP ; CAR ; DIP { CDR } } ; IF_LEFT { ADD } { SUB } ; NIL operation ; PAIR }; RUN: use %default; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; SUB: pop 5, 6; push -1; NIL: push []; PAIR: pop [], -1; push ([], -1);
Pretty simple, we just replace (Left 5)
with (Right 5)
! The result of the operation (-1
) indicates that the contract subtracted the two values instead of adding them together!
Now that you understood the bases of using a union type as a parameter, we can introduce annotations to make the code a little easier to read:
parameter (or (int %increment) (int %decrement)) ;
storage int ;
code {
UNPAIR ;
IF_LEFT
{ ADD }
{ SUB } ;
NIL operation ;
PAIR ;
} ;
RUN %increment 5 6 ;
stdout
parameter (or (int %increment) (int %decrement)); storage int; code { { DUP ; CAR ; DIP { CDR } } ; IF_LEFT { ADD } { SUB } ; NIL operation ; PAIR }; RUN: use %increment; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
This contract does the same thing as the one before, the difference is that you can annotate the arguments of the union value and when running the contract, you can use the annotations to target the value of your choice. If you replace RUN %increment 5 6
with RUN %decrement 5 6
, you will obtain -1
.
Each side of the union type represents the "entrypoint" of the contract and using annotations allows us to identify these entrypoints more clearly. We can say our contract has an "increment" entrypoint and a "decrement" entrypoint. An entrypoint in a Michelson contract is nothing but the left or right side of a union type with an annotation. This only works if every annotation is unique.
Now imagine, you want a third entrypoint, for example to reset the storage to zero. Union types only allow two values (left/right). However, nested union types are a thing in Michelson and are widely used in contracts! Let's see how it would look like first without annotations:
parameter (or (or int int) unit) ;
storage int ;
code {
UNPAIR ;
IF_LEFT
{
IF_LEFT
{ ADD }
{ SUB } ;
}
{ DROP ; DROP ; PUSH int 0 } ;
NIL operation ;
PAIR ;
} ;
RUN %default (Left (Left 5)) 6 ;
stdout
parameter (or (or int int) unit); storage int; code { { DUP ; CAR ; DIP { CDR } } ; IF_LEFT { IF_LEFT { ADD } { SUB } } { DROP ; DROP ; PUSH int 0 } ; NIL operation ; PAIR }; RUN: use %default; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; IF_LEFT: pop 5; push 5; ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
The first IF_LEFT
unwraps the first union value and the second one unwraps the second union value that decides if the contract is going to add the values or subtract them. This contract adds the two values but you can easily subtract by replacing RUN %default (Left (Left 5)) 6 ;
with RUN %default (Left (Right 5)) 6 ;
. If you want to reset the storage to 0
, you can write RUN %default (Right Unit) 6 ;
.
Here is the same contract written with annotations:
parameter (or (or (int %increment) (int %decrement)) (unit %reset)) ;
storage int ;
code {
UNPAIR ;
IF_LEFT
{
IF_LEFT
{ ADD }
{ SUB } ;
}
{ DROP ; DROP ; PUSH int 0 } ;
NIL operation ;
PAIR ;
} ;
RUN %increment 5 6 ;
stdout
parameter (or (or (int %increment) (int %decrement)) (unit %reset)); storage int; code { { DUP ; CAR ; DIP { CDR } } ; IF_LEFT { IF_LEFT { ADD } { SUB } } { DROP ; DROP ; PUSH int 0 } ; NIL operation ; PAIR }; RUN: use %increment; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; IF_LEFT: pop 5; push 5; ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
Obviously, you can make the parameters as complex as you wish, which is often the case with more complex smart contracts:
parameter (or (pair %add nat nat) (pair %sub nat nat)) ;
storage int ;
code {
CAR ;
IF_LEFT
{
## unpair the pair and add the numbers
UNPAIR ;
ADD ;
INT ;
}
{
## unpair the pair and subtract the numbers
UNPAIR ;
SUB ;
} ;
NIL operation ;
PAIR
} ;
RUN %add (Pair 5 6) 0 ;
stdout
parameter (or (pair :add nat nat) (pair :sub nat nat)); storage int; code { CAR ; IF_LEFT { { DUP ; CAR ; DIP { CDR } } ; ADD ; INT } { { DUP ; CAR ; DIP { CDR } } ; SUB } ; NIL operation ; PAIR }; RUN: use %add; parameter (or (pair :add nat nat) (pair :sub nat nat)); storage int; code { CAR ; IF_LEFT { { DUP ; CAR ; DIP { CDR } } ; ADD ; INT } { { DUP ; CAR ; DIP { CDR } } ; SUB } ; NIL operation ; PAIR }; RUN: use %add; parameter (or (pair %add nat nat) (pair %sub nat nat)); storage int; code { CAR ; IF_LEFT { { DUP ; CAR ; DIP { CDR } } ; ADD ; INT } { { DUP ; CAR ; DIP { CDR } } ; SUB } ; NIL operation ; PAIR }; RUN: use %add; drop all; push ((5, 6), 0); CAR: pop ((5, 6), 0); push (5, 6); IF_LEFT: pop (5, 6); push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); ADD: pop 5, 6; push 11; INT: pop 11; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
In this contract example, we pass a union value made of two pairs containing the values we want to add or subtract (instead of using the one in the storage). We tell the contract which one to use with RUN %add (Pair 5 6) 0
but we could also have written it RUN %default (Left (Pair 5 6)) 0
.
In addition to IF_LEFT
, you can also use IF_RIGHT
which is a macro that will just reverse the order of the element in the union value and run IF_LEFT
:
parameter (or (int %increment) (int %decrement)) ;
storage int ;
code {
UNPAIR ;
IF_RIGHT
{ SUB }
{ ADD } ;
NIL operation ;
PAIR ;
} ;
RUN %increment 5 6 ;
stdout
parameter (or (int %increment) (int %decrement)); storage int; code { { DUP ; CAR ; DIP { CDR } } ; { IF_LEFT { ADD } { SUB } } ; NIL operation ; PAIR }; RUN: use %increment; drop all; push (5, 6); DUP: push (5, 6); CAR: pop (5, 6); push 5; DIP: protect 1 item(s); CDR: pop (5, 6); push 6; restore 1 item(s); IF_LEFT: pop 5; push 5; ADD: pop 5, 6; push 11; NIL: push []; PAIR: pop [], 11; push ([], 11);
To finish with conditionals, there is a macro you can use to check if you want to verify that the correct side of the union type is used, ASSERT_LEFT
:
parameter (or (nat %increment) (unit %forbidden)) ;
storage nat ;
code {
UNPAIR ;
ASSERT_LEFT ;
ADD ;
NIL operation ;
PAIR
} ;
RUN %forbidden Unit 5 ;
stdout
parameter (or (nat %increment) (unit %forbidden)); storage nat; code { { DUP ; CAR ; DIP { CDR } } ; { IF_LEFT { RENAME } { { UNIT ; FAILWITH } } } ; ADD ; NIL operation ; PAIR }; RUN: use %forbidden; drop all; push (Unit, 5); DUP: push (Unit, 5); CAR: pop (Unit, 5); push Unit; DIP: protect 1 item(s); CDR: pop (Unit, 5); push 5; restore 1 item(s); IF_LEFT: pop Unit; push Unit; UNIT: push Unit; FAILWITH: pop Unit;
stderr
MichelsonRuntimeError: Unit at RUN -> IF_LEFT -> FAILWITH
As you can see, the contract fails if we try to call the forbidden entrypoint. If you switch it to increment and pass a number instead of Unit
, it will work!
So far, we have only received union values in the parameters, but it is also possible to create them within the smart contracts. That can be very useful when building a new transaction if you are targetting the entrypoint of another contract. Here is how you can easily set a value of type union
:
parameter unit ;
storage unit ;
BEGIN Unit Unit ;
DROP ;
## By pushing it
PUSH (or string nat) (Left "hello");
## By building it
PUSH int 6 ;
LEFT string ;
DUMP ;
stdout
parameter unit; storage unit; BEGIN: use %default; drop all; push (Unit, Unit); DROP: pop (Unit, Unit); PUSH: push hello; PUSH: push 6; LEFT: pop 6; push 6;
A union type
, like an int
or a string
is a pushable value, which means that you can use the PUSH
instruction to add a new value onto the stack. If you want to add a new union value, you can use PUSH
followed by the type of the union and its value. Although the union type is a value than can be of two different types, it is always one single value, this is why when you push the value onto the stack, you must use (Left value)
or (Right value)
to initialize it.
If you want to use a value on the stack to create the union value, use LEFT
to add it to the left side of the union or RIGHT
to add it to the right side and add the expected type of the other side after the instruction.
The union
type is a powerful concept to create entrypoints in your contract. You can use as many nested union values as you need to redirect the execution of the contract according to the value sent as a parameter.
Union types are so useful that it is also possible to use them in ways you may not have imagined before this tutorial! One of the possible applications of union
types is the implementation of enum
-like values. Let's consider the following contract:
parameter (or (or (unit %UP) (unit %DOWN)) (or (unit %LEFT) (unit %RIGHT))) ;
storage unit ;
code {
CAR ;
IF_LEFT
{
IF_LEFT
{ PUSH string "UP" ; FAILWITH }
{ PUSH string "DOWN" ; FAILWITH }
}
{
IF_LEFT
{ PUSH string "LEFT" ; FAILWITH }
{ PUSH string "RIGHT" ; FAILWITH }
}
} ;
RUN %default (Left (Right Unit)) Unit ;
stdout
parameter (or (or (unit %UP) (unit %DOWN)) (or (unit %LEFT) (unit %RIGHT))); storage unit; code { CAR ; IF_LEFT { IF_LEFT { PUSH string "UP" ; FAILWITH } { PUSH string "DOWN" ; FAILWITH } } { IF_LEFT { PUSH string "LEFT" ; FAILWITH } { PUSH string "RIGHT" ; FAILWITH } } }; RUN: use %default; drop all; push (Unit, Unit); CAR: pop (Unit, Unit); push Unit; IF_LEFT: pop Unit; push Unit; IF_LEFT: pop Unit; push Unit; PUSH: push DOWN; FAILWITH: pop DOWN;
stderr
MichelsonRuntimeError: DOWN at RUN -> IF_LEFT -> IF_LEFT -> FAILWITH
The contract itself is not very interesting but it demonstrates the logic of creating enums
with union
values. Here, a nested union value allows us to test a given state that can have 4 different values. By comparison, if we wanted to use boolean
values, we would have to set 2 separate values in the parameters, probably provide them as a pair
which would force us to UNPAIR
the parameter first before running 2 separate conditions. The solution with a union
is cleaner and more effective.
← Chapter 11 Chapter 13 →