Tipe Data

Setiap nilai di Cairo memiliki jenis data tertentu, yang memberi tahu Cairo jenis data apa yang sedang ditentukan sehingga Cairo tahu cara bekerja dengan data tersebut. Bagian ini mencakup dua subset jenis data: skalar dan compound.

Ingatlah bahwa Cairo adalah bahasa berjenis statis, yang berarti bahwa ia harus mengetahui jenis-jenis semua variabel pada waktu kompilasi. Kompiler biasanya dapat menyimpulkan jenis yang diinginkan berdasarkan nilai dan penggunaannya. Dalam kasus-kasus di mana banyak jenis mungkin terjadi, kita dapat menggunakan metode konversi (cast) di mana kita menentukan jenis keluaran yang diinginkan.

fn main() {
    let x: felt252 = 3;
    let y: u32 = x.try_into().unwrap();
}

Anda akan melihat anotasi jenis yang berbeda untuk jenis data lainnya.

Tipe Scalar

Jenis data scalar mewakili sebuah nilai tunggal. Cairo memiliki tiga jenis skalar utama: felts, bilangan bulat, dan boolean. Anda mungkin mengenali ini dari bahasa pemrograman lain. Mari kita lihat bagaimana cara kerjanya di Cairo.

Tipe Felt

In Cairo, if you don't specify the type of a variable or argument, its type defaults to a field element, represented by the keyword felt252. In the context of Cairo, when we say “a field element” we mean an integer in the range \( 0 \leq x < P \), where \( P \) is a very large prime number currently equal to \( {2^{251}} + 17 \cdot {2^{192}} + 1 \). When adding, subtracting, or multiplying, if the result falls outside the specified range of the prime number, an overflow (or underflow) occurs, and an appropriate multiple of \( P \) is added or subtracted to bring the result back within the range (i.e., the result is computed \( \mod P \) ).

The most important difference between integers and field elements is division: Division of field elements (and therefore division in Cairo) is unlike regular CPUs division, where integer division \( \frac{x}{y} \) is defined as \( \left\lfloor \frac{x}{y} \right\rfloor \) where the integer part of the quotient is returned (so you get \( \frac{7}{3} = 2 \)) and it may or may not satisfy the equation \( \frac{x}{y} \cdot y == x \), depending on the divisibility of x by y.

In Cairo, the result of \( \frac{x}{y} \) is defined to always satisfy the equation \( \frac{x}{y} \cdot y == x \). If y divides x as integers, you will get the expected result in Cairo (for example \( \frac{6}{2} \) will indeed result in 3). But when y does not divide x, you may get a surprising result: for example, since \( 2 \cdot \frac{P + 1}{2} = P + 1 \equiv 1 \mod P \), the value of \( \frac{1}{2} \) in Cairo is \( \frac{P + 1}{2} \) (and not 0 or 0.5), as it satisfies the above equation.

Tipe Integer

The felt252 type is a fundamental type that serves as the basis for creating all types in the core library. However, it is highly recommended for programmers to use the integer types instead of the felt252 type whenever possible, as the integer types come with added security features that provide extra protection against potential vulnerabilities in the code, such as overflow and underflow checks. By using these integer types, programmers can ensure that their programs are more secure and less susceptible to attacks or other security threats. An integer is a number without a fractional component. This type declaration indicates the number of bits the programmer can use to store the integer. Table 3-1 shows the built-in integer types in Cairo. We can use any of these variants to declare the type of an integer value.

PanjangTidak bertanda
8-bitu8
16-bitu16
32-bitu32
64-bitu64
128-bitu128
256-bitu256
32-bitusize

Table 3-1: Integer Types in Cairo.

Setiap varian memiliki ukuran yang eksplisit. Perlu diperhatikan bahwa untuk saat ini, jenis data usize hanyalah alias untuk u32; namun, ini mungkin berguna jika di masa depan Cairo dapat dikompilasi ke MLIR. Karena variabel-variabel bersifat tidak bertanda, mereka tidak dapat berisi angka negatif. Kode ini akan menyebabkan program mengalami panic:

fn sub_u8s(x: u8, y: u8) -> u8 {
    x - y
}

fn main() {
    sub_u8s(1, 3);
}

All integer types previously mentioned fit into a felt252, except for u256 which needs 4 more bits to be stored. Under the hood, u256 is basically a struct with 2 fields: u256 {low: u128, high: u128}.

Cairo also provides support for signed integers, starting with the prefix i. These integers can represent both positive and negative values, with sizes ranging from i8 to i128. Each signed variant can store numbers from \( -({2^{n - 1}}) \) to \( {2^{n - 1}} - 1 \) inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from \( -({2^7}) \) to \( {2^7} - 1 \), which equals -128 to 127.

You can write integer literals in any of the forms shown in Table 3-2. Note that number literals that can be multiple numeric types allow a type suffix, such as 57_u8, to designate the type. It is also possible to use a visual separator _ for number literals, in order to improve code readability.

Literal NumerikContoh
Decimal98222
Hex0xff
Octal0o04321
Binary0b01

Table 3-2: Integer Literals in Cairo.

Jadi, bagaimana Anda tahu jenis bilangan bulat mana yang harus digunakan? Cobalah untuk memperkirakan nilai maksimum yang dapat dimiliki oleh integer Anda dan pilih ukuran yang sesuai. Situasi utama di mana Anda akan menggunakan usize adalah saat mengindeks suatu jenis koleksi.

Operasi Numerik

Cairo mendukung operasi matematika dasar yang dapat Anda harapkan untuk semua jenis bilangan bulat: penambahan, pengurangan, perkalian, pembagian, dan sisa. Pembagian bilangan bulat memutuskan ke nol ke bilangan bulat terdekat. Kode berikut menunjukkan bagaimana Anda akan menggunakan setiap operasi numerik dalam pernyataan let:

fn main() {
    // addition
    let sum = 5_u128 + 10_u128;

    // subtraction
    let difference = 95_u128 - 4_u128;

    // multiplication
    let product = 4_u128 * 30_u128;

    // division
    let quotient = 56_u128 / 32_u128; //result is 1
    let quotient = 64_u128 / 32_u128; //result is 2

    // remainder
    let remainder = 43_u128 % 5_u128; // result is 3
}

Setiap ekspresi dalam pernyataan ini menggunakan operator matematika dan dievaluasi menjadi nilai tunggal, yang kemudian diikat ke dalam suatu variabel.

Appendix B contains a list of all operators that Cairo provides.

Tipe Data Boolean

As in most other programming languages, a Boolean type in Cairo has two possible values: true and false. Booleans are one felt252 in size. The Boolean type in Cairo is specified using bool. For example:

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

When declaring a bool variable, it is mandatory to use either true or false literals as value. Hence, it is not allowed to use integer literals (i.e. 0 instead of false) for bool declarations.

The main way to use Boolean values is through conditionals, such as an if expression. We’ll cover how if expressions work in Cairo in the "Control Flow" section.

String Types

Cairo doesn't have a native type for strings but provides two ways to handle them: short strings using simple quotes and ByteArray using double quotes.

Short strings

A short string is an ASCII string where each character is encoded on one byte (see the ASCII table). For example:

  • 'a' is equivalent to 0x61
  • 'b' is equivalent to 0x62
  • 'c' is equivalent to 0x63
  • 0x616263 is equivalent to 'abc'.

Cairo uses the felt252 for short strings. As the felt252 is on 251 bits, a short string is limited to 31 characters (31 * 8 = 248 bits, which is the maximum multiple of 8 that fits in 251 bits).

You can choose to represent your short string with an hexadecimal value like 0x616263 or by directly writing the string using simple quotes like 'abc', which is more convenient.

Here are some examples of declaring short strings in Cairo:

fn main() {
    let my_first_char = 'C';
    let my_first_char_in_hex = 0x43;

    let my_first_string = 'Hello world';
    let my_first_string_in_hex = 0x48656C6C6F20776F726C64;

    let long_string: ByteArray = "this is a string which has more than 31 characters";
}

Byte Array Strings

Cairo's Core Library provides a ByteArray type for handling strings and byte sequences longer than short strings. This type is particularly useful for longer strings or when you need to perform operations on the string data.

The ByteArray in Cairo is implemented as a combination of two parts:

  1. An array of bytes31 words, where each word contains 31 bytes of data.
  2. A pending felt252 word that acts as a buffer for bytes that haven't yet filled a complete bytes31 word.

This design enables efficient handling of byte sequences while aligning with Cairo's memory model and basic types. Developers interact with ByteArray through its provided methods and operators, abstracting away the internal implementation details.

Unlike short strings, ByteArray strings can contain more than 31 characters and are written using double quotes:

fn main() {
    let my_first_char = 'C';
    let my_first_char_in_hex = 0x43;

    let my_first_string = 'Hello world';
    let my_first_string_in_hex = 0x48656C6C6F20776F726C64;

    let long_string: ByteArray = "this is a string which has more than 31 characters";
}

Compound Types

Tipe Data Tuple

tuple adalah cara umum untuk mengelompokkan sejumlah nilai dengan berbagai jenis ke dalam satu tipe data gabungan. Tupel memiliki panjang tetap: setelah dideklarasikan, mereka tidak dapat tumbuh atau menyusut dalam ukuran.

Kita membuat sebuah tupel dengan menulis daftar nilai yang dipisahkan oleh koma di dalam tanda kurung. Setiap posisi dalam tupel memiliki tipe, dan tipe dari nilai-nilai yang berbeda dalam tupel tidak harus sama. Kami telah menambahkan anotasi tipe opsional dalam contoh ini:

fn main() {
    let tup: (u32, u64, bool) = (10, 20, true);
}

Variabel tup terikat pada seluruh tupel karena tupel dianggap sebagai satu elemen gabungan. Untuk mendapatkan nilai-nilai individual dari sebuah tupel, kita dapat menggunakan pola pencocokan untuk mendestruksi nilai tupel, seperti ini:

fn main() {
    let tup = (500, 6, true);

    let (x, y, z) = tup;

    if y == 6 {
        println!("y is 6!");
    }
}

This program first creates a tuple and binds it to the variable tup. It then uses a pattern with let to take tup and turn it into three separate variables, x, y, and z. This is called destructuring because it breaks the single tuple into three parts. Finally, the program prints y is 6! as the value of y is 6.

We can also declare the tuple with value and types, and destructure it at the same time. For example:

fn main() {
    let (x, y): (felt252, felt252) = (2, 3);
}

The Unit Type ()

Tipe unit adalah tipe yang hanya memiliki satu nilai (). Ini direpresentasikan oleh sebuah tupel tanpa elemen. Ukurannya selalu nol, dan dipastikan tidak ada dalam kode yang telah dikompilasi.

You might be wondering why you would even need a unit type? In Cairo, everything is an expression, and an expression that returns nothing actually returns () implicitly.

The Fixed Size Array Type

Another way to have a collection of multiple values is with a fixed size array. Unlike a tuple, every element of a fixed size array must have the same type.

We write the values in a fixed-size array as a comma-separated list inside square brackets. The array’s type is written using square brackets with the type of each element, a semicolon, and then the number of elements in the array, like so:

fn main() {
    let arr1: [u64; 5] = [1, 2, 3, 4, 5];
}

In the type annotation [u64; 5], u64 specifies the type of each element, while 5 after the semicolon defines the array's length. This syntax ensures that the array always contains exactly 5 elements of type u64.

Fixed size arrays are useful when you want to hardcode a potentially long sequence of data directly in your program. This type of array must not be confused with the Array<T> type, which is a similar collection type provided by the core library that is allowed to grow in size. If you're unsure whether to use a fixed size array or the Array<T> type, chances are that you are looking for the Array<T> type.

Because their size is known at compile-time, fixed-size arrays don't require runtime memory management, which makes them more efficient than dynamically-sized arrays. Overall, they're more useful when you know the number of elements will not need to change. For example, they can be used to efficiently store lookup tables that won't change during runtime. If you were using the names of the month in a program, you would probably use a fixed size array rather than an Array<T> because you know it will always contain 12 elements:

    let months = [
        'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
        'October', 'November', 'December',
    ];

You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets, as shown here:

    let a = [3; 5];

The array named a will contain 5 elements that will all be set to the value 3 initially. This is the same as writing let a = [3, 3, 3, 3, 3]; but in a more concise way.

Accessing Fixed Size Arrays Elements

As a fixed-size array is a data structure known at compile time, it's content is represented as a sequence of values in the program bytecode. Accessing an element of that array will simply read that value from the program bytecode efficiently.

We have two different ways of accessing fixed size array elements:

  • Deconstructing the array into multiple variables, as we did with tuples.
fn main() {
    let my_arr = [1, 2, 3, 4, 5];

    // Accessing elements of a fixed-size array by deconstruction
    let [a, b, c, _, _] = my_arr;
    println!("c: {}", c); // c: 3    
}
  • Converting the array to a Span, that supports indexing. This operation is free and doesn't incur any runtime cost.
fn main() {
    let my_arr = [1, 2, 3, 4, 5];

    // Accessing elements of a fixed-size array by index
    let my_span = my_arr.span();
    println!("my_span[2]: {}", my_span[2]); // my_span[2]: 3
}

Note that if we plan to repeatedly access the array, then it makes sense to call .span() only once and keep it available throughout the accesses.

Tipe Data casting

Cairo addresses conversion between types by using the try_into and into methods provided by the TryInto and Into traits from the core library. There are numerous implementations of these traits within the standard library for conversion between types, and they can be implemented for custom types as well.

Into

The Into trait allows for a type to define how to convert itself into another type. It can be used for type conversion when success is guaranteed, such as when the source type is smaller than the destination type.

To perform the conversion, call var.into() on the source value to convert it to another type. The new variable's type must be explicitly defined, as demonstrated in the example below.

fn main() {
    let my_u8: u8 = 10;
    let my_u16: u16 = my_u8.into();
    let my_u32: u32 = my_u16.into();
    let my_u64: u64 = my_u32.into();
    let my_u128: u128 = my_u64.into();

    let my_felt252 = 10;
    // As a felt252 is smaller than a u256, we can use the into() method
    let my_u256: u256 = my_felt252.into();
    let my_other_felt252: felt252 = my_u8.into();
    let my_third_felt252: felt252 = my_u16.into();
}

TryInto

Similar to Into, TryInto is a generic trait for converting between types. Unlike Into, the TryInto trait is used for fallible conversions, and as such, returns Option<T>. An example of a fallible conversion is when the target type might not fit the source value.

Also similar to Into is the process to perform the conversion; just call var.try_into() on the source value to convert it to another type. The new variable's type also must be explicitly defined, as demonstrated in the example below.

fn main() {
    let my_u256: u256 = 10;

    // Since a u256 might not fit in a felt252, we need to unwrap the Option<T> type
    let my_felt252: felt252 = my_u256.try_into().unwrap();
    let my_u128: u128 = my_felt252.try_into().unwrap();
    let my_u64: u64 = my_u128.try_into().unwrap();
    let my_u32: u32 = my_u64.try_into().unwrap();
    let my_u16: u16 = my_u32.try_into().unwrap();
    let my_u8: u8 = my_u16.try_into().unwrap();

    let my_large_u16: u16 = 2048;
    let my_large_u8: u8 = my_large_u16.try_into().unwrap(); // panics with 'Option::unwrap failed.'
}