Skip to main content
Version: Next

Interoperability

LIGO can work together with other smart contract languages on Tezos, such as calling them and receiving calls from them. However, data structures from different high-level languages might have different representations in Michelson and not how LIGO structures them by default. When a LIGO contract calls contracts that were written in other high-level languages, it must pass parameters that are compatible with the data structures in the other contracts.

Michelson types and annotations

Michelson types consist of or types and pair types, combined with field annotations. Field annotations add constraints on a Michelson type. To be compatible, two types must meet these criteria:

  • They must have the same types in the same structure
  • Their annotations must be compatible, which means that one of the following is true:
    • Both types have no annotations
    • Both types have annotations that are identical
    • One type has annotations and the other type has no annotations

For example, this Michelson type is a pair that contains an integer and a string, both with annotations:

(pair (int %foo) (string %bar))

That type is compatible with this type because it is identical:

(pair (int %foo) (string %bar))

It is also compatible with this type because it has the same types minus the annotations:

(pair int string)

However, it is not compatible with the following type because the primitive types have a different structure, even though the annotations are on the same types:

(pair (string %bar) (int %foo))

It is also not compatible with the following type even though the following type uses the same primitive types in the same structure. The annotations don't match and therefore the types don't match.

(pair (int %bar) (string %foo))

It is also not compatible with the following type because this type has some annotations but not all of the annotations in the other type:

(pair (int %foo) (string))

In this way, to call another contract, you may need to change the output Michelson format of your types to match the type that the other contract expects.

info

When possible, use annotations when you define or call entrypoints. Annotations help make it clear which entrypoint you are referring to.

Setting the Michelson layout of LIGO data structures

LIGO can format types in Michelson in two basic layouts: combs and trees. For more information about combs and trees, see Pairs on docs.tezos.com.

Right comb layout

Since version 1.0, the default Michelson data representation of LIGO data structures is a right comb that matches the order of the LIGO declarations. You can use the decorator @layout("comb") to make this choice explicit, as in this example of a variant type:

type animal =
@layout("comb")
| ["Elephant"]
| ["Dog"]
| ["Cat"];

The resulting Michelson code looks like this:

(or
(unit %elephant)
(or (unit %dog)
(unit %cat)))

The decorator @layout("comb") can also be used on record types:

type artist =
@layout("comb")
{
genre : string,
since : timestamp,
name : string
};

Unlike the variant type, the compiled Michelson code of a record type in a right comb is a nested pair. The previous example looks like this in Michelson:

(pair (string %genre) (timestamp %since) (string %name))

Left-balanced tree layout

Prior to version 1.0, LIGO formatted data types into alphabetically ordered left-balanced trees by default. You can get the equivalent in version 1.0 and later with the decorator @layout("tree"), as in this example:

type animal =
@layout("tree")
| ["Elephant"]
| ["Dog"]
| ["Cat"];

The resulting Michelson code looks like this:

(or
(or
(unit %cat)
(unit %dog))
(unit %elephant))

Note that the variant cases are ordered alphabetically.

Setting Michelson annotations

To specify the annotation for a LIGO declaration, use the @annot decorator. For example, this variant type has custom annotations on each case:

type animal =
| @annot("memory") ["Elephant"]
| @annot("face") ["Dog"]
| @annot("fish") ["Cat"]

The resulting Michelson looks like this:

(or
(or
(unit %fish)
(unit %face))
(unit %memory))

The decorator @annot("<name>") can also be used on record field annotations:

type artist = {
@annot("style") genre: string,
@annot("from") since: timestamp,
@annot("performer") name: string
}

If the @layout and @annot decorators are not adequate for your use case, you can format Michelson types manually as described in the next section.

Manually formatting Michelson types

To interoperate with existing Michelson code or to be compatible with certain development tooling, LIGO has two special interoperation types: michelson_or and michelson_pair. These types provide the flexibility to model the exact Michelson output, including field annotations.

The LIGO michelson_or type creates a Michelson or type from two LIGO types and their annotations. Similarly, the LIGO michelson_pair type creates a Michelson pair type. In either case, you must provide annotations for both types, but if you provide an empty string, LIGO omits the annotation in the Michelson code.

Take for example the following Michelson type that we want to interoperate with:

(or
(unit %z)
(or %other
(unit %y)
(pair %other
(string %x)
(pair %other
(int %w)
(nat %v)))))

To reproduce this type we can use the following LIGO code:

type w_and_v = michelson_pair<[int, "w", nat, "v"]>;
type x_and = michelson_pair<[string, "x", w_and_v, "other"]>;
type y_or = michelson_or<[unit, "y", x_and, "other"]>;
type z_or = michelson_or<[unit, "z", y_or, "other"]>;
info

Alternatively, if annotations are not important you can also use plain tuples for pairs instead. Plain tuples do not have any annotations.

To use variables of type michelson_or you have to use M_left and M_right. M_left picks the left or case while M_right picks the right or case. For michelson_pair you need to use tuples.

let z : z_or = M_left(unit);
let y_1 : y_or = M_left(unit);
let y : z_or = M_right(y_1);
let x_pair = ["foo", [2, 3n]];
let x_1 : y_or = M_right (x_pair);
let x : z_or = M_right (y_1);

Manual data structure conversion

If you want to get your hands dirty, it is also possible to do manual data structure conversion. The following code provides some examples:

type z_to_v =
["Z"]
| ["Y"]
| ["X"]
| ["W"]
| ["V"];
type w_or_v = michelson_or<[unit, "w", unit, "v"]>;
type x_or = michelson_or<[unit, "x", w_or_v, "other"]>;
type y_or = michelson_or<[unit, "y", x_or, "other"]>;
type z_or = michelson_or<[unit, "z", y_or, "other"]>;
type test = {
z: string,
y: int,
x: string,
w: bool,
v: int
};
const make_concrete_sum = (r: z_to_v): z_or =>
match(r) {
when(Z()): M_left(unit);
when(Y()): M_right(M_left(unit));
when(X()): M_right (M_right (M_left(unit)));
when(W()): M_right (M_right (M_right(M_left(unit))));
when(V()): M_right (M_right (M_right(M_right(unit))))
};
const make_concrete_record = (r: test) =>
[r.z, r.y, r.x, r.w, r.v];
const make_abstract_sum = (z_or: z_or): z_to_v =>
match(z_or) {
when(M_left(n)): Z();
when(M_right(y_or)): match(y_or) {
when(M_left(n)): Y();
when(M_right(x_or)): match(x_or) {
when(M_left(n)): X();
when(M_right(w_or)): match(w_or) {
when(M_left(n)): W();
when(M_right(n)): V()
}
}
}
};
const make_abstract_record =
(z: string, y: int, x: string, w: bool, v: int) => ({z,y,x,w,v});

Entrypoints and annotations

When a contract has multiple entrypoints, LIGO implicitly creates a parameter for it with a variant type. For example, the following contract has entrypoints named left and right:

type storage = int
@entry
const left = (i: int, x: storage) : [list<operation>, storage] =>
[[], x - i]
@entry
const right = (i: int, x: storage) : [list<operation>, storage] =>
[[], x + i]

Internally, LIGO structures the contract with a parameter type:

type storage = int;
type parameter =
["Left", int]
| ["Right", int];
let main = (p: parameter, x: storage): [list<operation>, storage] =>
[list ([]), match(p) {
when(Left(i)): x - i;
when(Right(i)): x + i
}
];

This contract can be called by another contract, like this one:

type storage = int;
type parameter = int;
type x = | ["Left", int];
@entry
const main = (p: parameter, s: storage): [list<operation>, storage] => {
let contract =
match (Tezos.get_entrypoint_opt("%left", "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx")) {
when(Some(c)): c;
when(None()): failwith ("contract does not match")
};
return [
[Tezos.transaction(Left(2), 2mutez, contract)],
s];
};

The calling contract uses the entrypoint with the %left annotation without mentioning the %right entrypoint. LIGO uses the annotation to determine which side of the pair to put the int type into.

This currently only works for or's and variant types in LIGO.

Amendment

With amendment 007 to Tezos this is changed though, and also pairs can be ordered differently.