Michelson Interoperability in Ligo

Claude Barde
Oct 19, 2020 · 9 min read

How to be sure your Ligo code compiles to the Michelson code you want

Image for post
Image for post
Michelson code

Ligo is a fantastic tool that makes working with Tezos smart contracts a lot easier. However, its ease of use means that the compiler takes care of the heavy lifting for you and may compile your code down to a format you may not want. This becomes particularly important with TZIP standards: these standards establish fixed Michelson structures that could make your code useless if you don’t respect them. Here is a very simple example:

Imagine you have a record in Ligo like this one:

let my_record = {
owner: "tz1...";
balance: 100n;
name: "John Smith";
}

Ultimately, a record will be compiled to a pair (because Michelson doesn’t know records). In this example, 2 different nested pairs would be possible: (pair %balance 100n (pair (%name "John Smith") (%owner "tz1..."))) or (pair (pair (%balance 100n) (%name "John Smith")) %owner "tz1..."). So how can you force Ligo to give you one or the other? This is the subject of this article!

This article will present different ways of “thinking in Michelson” while programming with Ligo. These methods help you follow a defined standard and make all the pieces of the ecosystem work better together as they follow the same rules. Ligo can create pairs and union values directly, it can convert existing records into pairs or union values with the correct structure and it can even let you use Michelson directly within your Ligo code!

As it is generally the case in my Ligo articles, we will use the CameLigo syntax for the examples. However, the logic is the same for the different syntaxes and you can easily adapt these examples to the syntax of your choice!

Let’s see how this all works!

Creating Michelson types and values

As use cases for the Tezos blockchain evolve, you may be required to use code that is not yours. For example, you want to get data from an oracle that takes a complex pair as a parameter. In the case of a Harbinger contract, the returned value is of type (pair string (pair timestamp nat)) and you are wondering: how can I tell Ligo this is the kind of pair I expect when pair is not a type in Ligo?

In order to prepare for these complex interactions, Ligo introduced types, converters and functions to work seamlessly with Michelson. If you wish, you can create pair and union types yourself with michelson_pair and michelson_or. This is how they work:

Image for post
Image for post
Creating Michelson pairs in Ligo

There are different steps to follow in order to create Michelson pairs/unions with Ligo:

  1. type my_new_pair|my_new_union: it creates a new type for the Michelson pair or union.
  2. (type, “annotation”, type, “annotation”): a pair or a union type is made of 2 typed fields. The type here refers to a Ligo type, if you want to include another pair or union, you will have to create its type first. If you don’t need the annotation, you can simply provide an empty string.
  3. michelson_pair|michelson_or: this is the converter that will take care of converting the data between parentheses into a pair or a union type.

For example:

type pair_of_hands = 
(string, "left", string, "right") michelson_pair
type union_of_hands =
(string, "left_hand", string, "right_hand") michelson_or

Creating new types is always fun but it is even better to use them! Using the new pair type is going to be as easy as writing a tuple:

let my_two_hands: pair_of_hands = ("the_left_one", "the_right"one")

Note: the types you use in the tuple must be the same as the ones you declared earlier.

To create a new union value, you will need two new helpers: M_left and M_right:

let my_left_hand: union_of_hands = 
(M_left (string): union_of_hands)
let my_right_hand: union_of_hands =
(M_right (string): union_of_hands)

Now, you can use the types and values in your code and be sure you have the right format in the Michelson output 🙌🏻

Converting Ligo types to Michelson types

The above examples work well if you have simple values that you want to convert easily. But what happens if you want to convert types that exist in Ligo but not in Michelson, like records or variants? You can use helper functions! Ligo provides you with a few functions that will help you switch from Ligo records and variants to Michelson pairs and unions.

Note: Ligo records are typically converted to pairs and variants to unions. However, you may prefer having more control over the format of the output and enforce a certain structure, namely left-combed or right-combed.

Converting records to pairs

Imagine the following record:

type customer = {
name: string;
age: nat;
wallet: mutez;
}

This kind of record will be converted to a nested pair, but we may have 2 different outputs:

  1. (pair (string %name) (pair (nat %age) (mutez %wallet))
  2. (pair (pair (nat %age) (mutez %wallet)) (string %name))

In general, it doesn’t matter too much but if you have to send this kind of data to another contract that expects a certain format, the transaction will fail if you don’t send the right format! In this scenario, you can use a helper function to convert your record into a pair with the format you prefer:

Image for post
Image for post

As it was the case in the previous part, you just have to declare a new type (the name of these types often include a reference to michelson to indicate their nature) followed by the type of the record you want to convert and the michelson_pair_left_comb converter. With this function, you will obtain:

(pair (pair (nat %age) (mutez %wallet)) (string %name))

If you want the nested pair on the right side, you can just replace michelson_pair_left_comb with michelson_pair_right_comb. And that’s it! If you work with more complex pairs, the nested pairs will always be on the side that you indicate, for example, the following record with michelson_pair_right_comb:

type customer = {
name: string;
age: nat;
wallet: mutez;
account: address;
createdOn: timestamp;
}

would become:

(pair (string %name) 
(pair (nat %age)
(pair (mutez %wallet)
(pair (address %account) (timestamp %createdOn)))))

Converting variants to unions

Variants are another type that you can find in Ligo but not in Michelson. They are typically converted to a union type and like the pair type, you may want to enforce a certain format. As you will notice below, it is very similar to what we have just done.

Let’s imagine we have the following variant:

type rsvp = 
| Will_come of nat
| Will_not_come of unit
| Doesnt_know of unit

Now, you want to make sure that the nested union will be on the left side of the main union, here is what you can do:

type rsvp_michelson = rsvp michelson_or_left_comb

The above line will then be converted to:

(or (or (nat %will_come) (unit %will_not_come)) (unit %doesnt_know))

If you want the nested union on the right side, you guessed it, you will use michelson_or_right_comb!

Using layout:comb

By default, all the pair and union types you create in Ligo are turned into alphabetically sorted left balanced trees but thanks to a recent update to the language, you can now easily turn these types into right combed trees without the use of the helper functions we introduced above. You just have to add [@layout:comb] after the equal sign to achieve this result.

For example, this non-annotated record:

type customer = {
name: string;
age: nat;
wallet: mutez;
account: address;
createdOn: timestamp;
}

would become by default the following pair:

(pair 
(pair
(pair
(pair
(nat %age)
(address %account))
(timestamp %createdOn))
(string %name))
(mutez %wallet))

However, if you only add [@layout:comb] to the previous record like that:

type customer = 
[@layout:comb] {
name: string;
age: nat;
wallet: mutez;
account: address;
createdOn: timestamp;
}

it will then be converted to the following right balanced pair:

(pair (string %name) 
(pair (nat %age)
(pair (mutez %wallet)
(pair (address %account) (timestamp %createdOn)))))

The same method applies to union types. The following variant:

type rsvp = 
[@layout:comb]
| Will_come of nat
| Will_not_come of unit
| Doesnt_know of unit

will become the following union type:

(or (nat %will_come) (or (unit %will_not_come) (unit %doesnt_know)))

instead of the default one:

(or (or (unit %doesnt_know) (nat %will_come)) (unit %will_not_come))

Converting Ligo values to Michelson values and vice-versa

The types are just a blueprint for the kind of format you want to give to your values, but ultimately, you want to use and manipulate values with these formats. You will need two helper functions that can convert Ligo values into Michelson values following the types you created and convert Michelson values into Ligo values following the same types.

Converting records into pairs

Let’s continue with the example of our record from earlier. Now, let’s create a record of type customer and let’s convert it to its matching Michelson type we created before:

Image for post
Image for post

As you can see, the conversion requires 4 pieces:

  • let customer_m: this is the variable that will hold our converted value
  • customer_michelson: the type we created earlier
  • Layout.convert_to_left_comb: this function takes a record or a variant and returns the appropriate value
  • customer: the record we want to convert

Note: the process would be exactly the same with a variant that you want to convert into an optional, first you declare the type and then you use it to convert a variant into an optional using Layout.convert_to_left_comb.

If you prefer a right-combed structure, you can use Layout.convert_to_right_comb instead of the previous converter.

Converting pairs into records

You may also want to convert the values created above back to records or variants. You may be passing these values around in your code or you may receive them from another contract (in the case of an oracle). In this case, it would be a lot easier to work with a record than with nested tuples. You will see that the steps to convert them back to more readable Ligo values are pretty similar to the ones used to convert them in the first place.

Let’s continue with our example and imagine we receive a value of type customer_michelson:

Image for post
Image for post

The process is very similar to the one presented earlier: you declare a variable of the record type you want and use Layout.convert_from_left_comb to convert the Michelson pair you have into its matching record.

Note: as it was the case before, it is the same process for variants/unions.

Embedding Michelson in Ligo

If needed, you can even embed Michelson directly into your Ligo code 🤯

I would say that this exercise is reserved for power users who know what they are doing but you can try it yourself to have a better understanding of the process. Here is the example given in the Ligo documentation:

Image for post
Image for post

This example is a function that uses Michelson instructions to add two nat values together. The michelson_add function is declared with the type of its argument (nat * nat) and its return type (nat). Next, you use [%Michelson … ] to enclose the rest of the code. Open a set of parentheses and enclose the Michelson code between a pair of {| … |} before annotating it with the expected parameter type and return type (like you did earlier with the Ligo function). To finish, you indicate the value that will be pushed to the stack before executing the Michelson code (our pair of nat).

Note: the Michelson code you inject is not typechecked and is assumed to be right. This will be particularly difficult to debug if you use a lot of Michelson instructions. Only use Michelson code that you know for sure is correct.

Conclusion

Although Ligo abstracts a lot of the complexity of Michelson so you can write your contracts in an easier and more effective way, it still offers different tools that give you the flexibility you need to write complex smart contracts. The ability to shape your pairs or unions in a precise fashion is amazing to use values within your contracts but also to pass these values between different contracts.

If you feel brave enough, you can mix up your knowledge of Ligo and Michelson to embed Michelson code directly into your Ligo code 👨‍💻👩‍💻

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Thanks to Sander Spies

Claude Barde

Written by

Traveler, translator and self-taught programmer; writing about the Tezos blockchain

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Claude Barde

Written by

Traveler, translator and self-taught programmer; writing about the Tezos blockchain

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store