Skip to content
This repository has been archived by the owner on Sep 25, 2024. It is now read-only.

Latest commit

 

History

History
181 lines (133 loc) · 7.98 KB

9. Ownership, references, snapshots I.md

File metadata and controls

181 lines (133 loc) · 7.98 KB

Ownership, References, Snapshots I

Amongst the numerous features Cairo1.0 inherits from Rust, we have the concept of Ownership. All computer programs have to effectively manage the way they use a computer's memory while running.

In most languages, there's an in-built garbage collector, that automatically looks for no-longer used memory as the program runs, while few other languages, expects the programmer to explicitly allocate and free memory, Cairo similar to Rust uses a totally different approach: managing memory using a set of Ownership rules that the compiler checks for.

If you've never programmed in Rust previously, the concept of Ownership might take some time to get used to. In this section, we are going to try to break down this concept in simpler terms!

Ownership

Ownership in Cairo, is a set of rules that governs how a Cairo program manages memory. To effectively understand how Ownership works, we'll need to look at understanding the Stack and the Heap.

The Stack and the Heap

The concept of the Stack and the Heap does not directly apply to Cairo, but is key to understanding the Ownership concept.

In simple terms, both the stack and the heap are parts of memory available to your code to use at runtime. The stack stores values in the order it gets them and removes them in the opposite order (last in, first out). A good example is piling up ceramic plates...you put them one untop another, but take them out in the opposite direction (of course trying to take out the first you put in from under the pile, could end up as a huge disaster)! It's important to note, that stacks can only store data of fixed sizes! Any data whose size is not known at compile time should be stored on the heap instead.

The heap unlike the stack is not very organized. When you put data on the heap, you're requesting for an amount of space from the memory allocator which in turn finds an empty spot big enough, marks it as in use, and returns a pointer to the location.

Pushing data to the stack is faster than to the heap, as the allocator doesn't have to spend resources searching for a space, but just piles untop, but with the heap you can always push data with unknown sizes at compile time.

Rules of Ownership

Whilst Ownership in Rust helps you effectively manage heap and stack data without needing to think of it often, Ownership in Cairo helps enforce safety checks for avoiding runtime errors, multiple writes to the same memory address, illegal memory addressing, etc. Here are some Ownership rules you need to note as we proceed:

  1. Each value in Cairo has an owner.
  2. There can only be one owner at every point in time.
  3. When the owner goes out of scope, the value is dropped.

Variable scopes

Scopes has to do with the range within a program for which an item is valid.

Here's an example that shows where a variable x would be valid:

// variable x is not valid here, as it's not yet declared
fn main() {
    let x = 'Starknet Africa';   // x becomes valid after declaration
} 
// x goes out of scope and is no longer valid

As you can see from the example above, a variable automatically goes out of scope once a function ends.

There are other more complex cases for Variable scopes that we'd be looking at below.

Moving Variables

When variables are passed to functions as arguments, or function return values are passed to a new variable, we say that the variable has been moved.

An example of moving variables into functions:

use array::ArrayTrait;
use debug::print;

fn main() {
    let mut array1 = ArrayTrait::new();   // we instantiate a new mutable array

    print_array(array1); // we pass `array1` as an argument to the `print_array` function. This operation moves `array1` out of scope of our `main` function.

    array1.append(2); // we get an error, as from here below `array1` is no longer valid
    array1.append(3);
    array1.append(4);

    print(array1);
}

fn print_array(mut arr: Array::<felt252>) {
    // `array1` comes in scope as it was moved to this function.
    print(arr); 
    // `array1` goes out of scope as it's passed again into the `print` function
}

The code above throws the error below:

    error: Variable was previously moved. Trait has no implementation in context: core::traits::Copy::<core::array::Array::<core::felt252>>
 --> tester.cairo:5:9
    let mut array1 = ArrayTrait::new();
        ^********^

Return values of functions are also moved when called, into the new variable assigned to the function call:

use array::ArrayTrait;
use debug::print;

fn main() {
    let mut array1 = ArrayTrait::new();  // we instantiate a new mutable array

    // `array1` is moved into the `print_array` function taking it out of scope, but immediately brought back to scope when returned and moved back into `array1`
    array1 = print_array(array1);

    // since `print_array` returned `array1`, it is now back in scope and can be further used without errors
    array1.append(2);

    // `array1` goes out of scope as it's passed again into the `print` function
    print(array1);
}

fn print_array(mut arr: Array::<felt252>) -> Array::<felt252> {
    arr
}

The Copy Trait

The Copy trait can be very useful in tackling ownership movement of variables. Types that implements the Copy trait do not get moved when passed to functions as arguments or returned from functions, but will instead pass a copy of the value.

To implement the Copy trait, you need to annotate the type iplementing it with the derive macro #derive(Copy).

NB: Its important to note that Arrays and Dictionaries can't implement this trait, but custom types such as Structs can, inasmuchas they do not contain Arrays or Dictionaries.

Here's an example:

#[derive(Copy)]
struct Student {
    name: felt252,
    age: u8,
    department: felt252,
}

fn main(student: Student) {
    get_student_name(student);
    get_student_age(student);
}

fn get_student_name(student: Student) -> felt252 {
    let Student {name, age, department} = student;
    name
}

fn get_student_age(student: Student) -> u8 {
    let Student {name, age, department} = student;
    age
}

As you can see, the struct Student implements the Copy trait, which is the reason why we can pass student to both the get_student_name and get_student_age function without encountering the variable moved error. This is because we are passing a copy of the student type to these functions rather than the student type.

The Drop Trait

Unlike Rust, Cairo does not automatically drop values when they go out of scope. This can be dangerous, as a key property of Cairo programs is soundness. Soundness means that if a statement during a program execution is false, then a cheating prover cannot convince an honest verifier that it is true. To enforce this, the compiler checks to ensure dictionaries are squashed (this occurs when ownership of the dictionary is moved to the squash method, thus allowing the dictionary to go out of scope). Unsquashed dictionaries are dangerous, as a malicious prover could manipulate the correctness of inconsistent updates.

However this can be curbed, by ensuring variables are dropped when they go out of scope. Variable dropping can be enforced by implementing the Drop trait. This can be implemented for all types except types containing dictionaries:

#[derive(Drop)]
struct Student {
    name: felt252,
    age: u8,
    department: felt252,
}

fn main(student: Student) {
    student;
}

Excluding the Drop trait throws the compiler error below:

    error: Variable not dropped. Trait has no implementation in context: core::traits::Drop::<src::tester::Student>
 --> tester.cairo:8:9
fn main(student: Student) {
        ^*****^

The Clone method

In Rust, the clone method is very useful for deeply copying heap data. In Cairo we can deeply copy the data of an Array using the clone method too:

use array::ArrayTrait;
use clone::Clone;
use array::ArrayTCloneImpl;

fn main() {
    let arr1 = ArrayTrait::new::<u128>();
    let arr2 = arr1.clone();
}