leo/docs/rfc/007-type-aliases.md
2021-09-22 10:16:01 -07:00

7.1 KiB

Leo RFC 007: Type Aliases

Authors

The Aleo Team.

Status

IMPLEMENTED

Summary

This RFC proposes the addition of type aliases to Leo, i.e. identifiers that abbreviate types and can be used wherever the latter can be used. A new top-level construct is proposed to define type aliases; no circularities are allowed. Type aliases are expanded away during compilation.

Motivation

Many programming languages provide the ability to create aliases (i.e. synonyms) of types, such as C's typedef. The purpose may be to abbreviate a longer type, such as an alias matrix for [i32; (3, 3)] in an application in which 3x3 matrices of 32-bit integers are relevant (e.g. for 3-D rotations, even though fractional numbers may be more realistic). The purpose may also be to clarify the intent and use of an existing type, such as an alias balance for u64 in an application that keeps track of balances.

The initial motivation that inspired this RFC (along with other RFCs) was the ability to have a type string for strings. Strings are arrays of characters according to RFC 001. With the array types of unspecified size proposed in RFC 006, [char; _] becomes a generic type for strings, which is desirable to alias with string.

Design

Syntax

The ABNF grammar changes as follows:

; modified rule:
keyword = ...
        / %s"string"
        / %s"type" ; new
        / %s"u8"
        / ...

; new rule:
type-alias-declaration = %s"type" identifier "=" type ";"

; modified rule:
declaration = import-declaration
            / function-declaration
            / circuit-declaration
            / constant-declaration
            / type-alias-declaration ; new


A type alias declaration introduces the identifier to stand for the type. Only top-level type alias declarations are supported; they are not supported inside functions or circuit types.

In addition, the following changes to the grammar are appropriate.

First, the rule

circuit-type = identifier / self-type ; replace with the one below

should be replaced with the rule

circuit-or-alias-type = identifier / self-type

The reason is that, at parsing time, an identifier is not necessarily a circuit type; it may be a type alias that may expand to a (circuit or non-circuit type). Thus, the nomenclature circuit-or-alias-type is appropriate. Consequently, references to circuit-type in the following rules must be replaced with circuit-or-alias-type:

; modified rule:
circuit-construction = circuit-or-alias-type "{" ; modified
                       circuit-inline-element
                       *( "," circuit-inline-element ) [ "," ]
                       "}"

; modified rule:
postfix-expression = primary-expression
                   / postfix-expression "." natural
                   / postfix-expression "." identifier
                   / identifier function-arguments
                   / postfix-expression "." identifier function-arguments
                   / circuit-or-alias-type "::" identifier function-arguments ; modified
                   / postfix-expression "[" expression "]"
                   / postfix-expression "[" [expression] ".." [expression] "]"

Second, the rule

aggregate-type = tuple-type / array-type / circuit-type

should be removed, because if we replaced circuit-type with circuit-or-alias-type there, the identifier could be a type alias, not necessarily an aggregate type. (The notion of aggregate type remains at a semantic level, but has no longer a place in the grammar.) Consequently, the rule

type = scalar-type / aggregate-type

should be rephrased as

type = scalar-type / tuple-type / array-type / circuit-or-alias-type

which "inlines" the previous aggregate-type with circuit-type replaced with circuit-or-alias-type.

Semantics

There must be no direct or indirect circularity in the type aliases. That is, it must be possible to expand all the type aliases away, obtaining an equivalent program without any type aliases.

Note that the built-in Self is a bit like a type alias, standing for the enclosing circuit type; and Self is replaced with the enclosing circuit type during canonicalization. Thus, canonicalization could be a natural place to expand user-defined type aliases; after all, type aliases introduce multiple ways to denote the same types (and not just via direct aliasing, but also via indirect aliasing, or via aliasing of components), and canonicalization serves exactly to reduce multiple ways to say the same thing to one canonical way.

On the other hand, expanding type aliases is more complicated than the current canonicalization transformations, which are all local and relatively simple. Expanding type aliases requires not only checking for circularities, but also to take into account references to type aliases from import declarations. For this reason, we may perform type alias expansion after canonicalization, such as just before type checking and inference. We could also make the expansion a part of the type checking and inference process, which already transforms the program by inferring missing types, so it could also expand type aliases away.

In any case, it seems beneficial to expand type aliases away (whether during canonicalization or as part or preamble to type checking and inference) prior to performing more processing of the program for eventual compilation to R1CS.

Examples

The aforementioned 3x3 matrix example could be written as follows:

type matrix = [u32; (3, 3)];

function matrix_multiply(x: matrix, y: matrix) -> matrix {
    ...
}

The aforementioned balance example could be written as follows:

type balance = u64;

function f(...) -> (..., balance, ...) {
    ...
}

The aforementioned string example could be written as follows:

type string = [char; _];

function f(str: string) -> ... {
    ...
}

Drawbacks

As other extensions of the language, this makes things inherently a bit more complicated.

Effect on Ecosystem

None; this is just a convenience for the Leo developer.

Alternatives

An alternative to creating a type alias

type T = U;

is to create a circuit type

circuit T { get: U }

that contains a single member variable.

This is clearly not equivalent to a type alias, because it involves conversions between T and U

T { get: u } // convert u:U to T
t.get // convert t:T to U

whereas a type alias involves no conversions: if T is an alias of U, then T and U are the same type, more precisely two syntactically different ways to designate the same semantic type.

While the conversions generally cause overhead in traditional programming languages, this may not be the case for Leo's compilation to R1CS, in which everything is flattened, including member variables of circuit types. Thus, it may be the case that the circuit T above reduces to just its member U in R1CS.

It might also be argued that wrapping a type into a one-member-variable circuit type could be a better practice than aliasing the type, to enforce better type separation and safety.

We need to consider the pros and cons of the two approaches, particularly in light of Leo's non-traditional compilation target.