What Is Ownership?
Cairo implements an ownership system to ensure the safety and correctness of its compiled code. The ownership mechanism complements the linear type system, which enforces that objects are used exactly once. This helps prevent common operations that can produce runtime errors, such as illegal memory address references or multiple writes to the same memory address, and ensures the soundness of Cairo programs by checking at compile time that all the dictionaries are squashed.
Now that we’re past basic Cairo syntax, we won’t include all the fn main() {
code in examples, so if you’re following along, make sure to put the following
examples inside a main
function manually. As a result, our examples will be a
bit more concise, letting us focus on the actual details rather than
boilerplate code.
Ownership Rules
First, let’s take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:
- Each value in Cairo has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Variable Scope
As a first example of ownership, we’ll look at the scope of some variables. A scope is the range within a program for which an item is valid. Take the following variable:
let s = 'hello';
The variable s
refers to a short string, where the value of the string is
hardcoded into the text of our program. The variable is valid from the point at
which it’s declared until the end of the current scope. Listing 3-1 shows a
program with comments annotating where the variable s
would be valid.
{ // s is not valid here, it’s not yet declared
let s = 'hello'; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
In other words, there are two important points in time here:
- When
s
comes into scope, it is valid. - It remains valid until it goes out of scope.
At this point, the relationship between scopes and when variables are valid is
similar to that in other programming languages. Now we’ll build on top of this
understanding by using the Array
type we introduced in the previous chapter.
Ownership with the Array
Type
To illustrate the rules of ownership, we need a data type that is more complex.
The types covered in the Data Types section
of Chapter 2 are of a known size, can be
quickly and trivially copied to make a new, independent instance if another
part of code needs to use the same value in a different scope, and can easily
be dropped when they're no longer used. But what is the behavior with the Array
type whose size
is unknown at compile time and which can't be trivially copied ?
Here is a short reminder of what an array looks like:
let mut arr = ArrayTrait::<u128>::new();
arr.append(1);
arr.append(2);
So, how does the ownership system ensure that each cell is never written to more than once? Consider the following code, where we try to pass the same instance of an array in two consecutive function calls:
use array::ArrayTrait;
fn foo(arr: Array<u128>) {
}
fn bar(arr:Array<u128>){
}
fn main() {
let mut arr = ArrayTrait::<u128>::new();
foo(arr);
bar(arr);
}
In this case, we try to pass the same array instance arr
by value to the functions foo
and bar
, which means
that the parameter used in both function calls is the same instance of the array. If you append a value to the array
in foo
, and then try to append another value to the same array in bar
, what would happen is that
you would attempt to try to write to the same memory cell twice, which is not allowed in Cairo.
To prevent this, the ownership of the arr
variable moves from the main
function to the foo
function. When trying
to call bar
with arr
as a parameter, the ownership of arr
was already moved to the first call. The ownership
system thus prevents us from using the same instance of arr
in foo
.
Running the code above will result in a compile-time error:
error: Variable was previously moved. Trait has no implementation in context: core::traits::Copy::<core::array::Array::<core::integer::u128>>
--> array.cairo:6:9
let mut arr = ArrayTrait::<u128>::new();
^*****^
The Copy
Trait
If a type implements the Copy
trait, passing it to a function will not move the ownership of the value to the function called, but will instead pass a copy of the value.
You can implement the Copy
trait on your type by adding the #[derive(Copy)]
annotation to your type definition. However, Cairo won't allow a type to be annotated with Copy if the type itself or any of its components don't implement the Copy trait.
While Arrays and Dictionaries can't be copied, custom types that don't contain either of them can be.
#[derive(Copy, Drop)]
struct Point {
x: u128,
y: u128,
}
fn main() {
let p1 = Point { x: 5, y: 10 };
foo(p1);
foo(p1);
}
fn foo(p: Point) {
// do something with p
}
In this example, we can pass p1
twice to the foo function because the Point
type implements the Copy
trait. This means that when we pass p1
to foo
, we are actually passing a copy of p1
, and the ownership of p1
remains with the main function.
If you remove the Copy
trait derivation from the Point
type, you will get a compile-time error when trying to compile the code.
Don't worry about the Struct
keyword. We will introduce this in Chapter 4.
The Drop
Trait
You may have noticed that the Point
type in the previous example also implements the Drop
trait. In Cairo, a value cannot go out of scope unless it has been previously moved.
For example, the following code will not compile, because the struct A
is not moved before it goes out of scope:
struct A {}
fn main() {
A {}; // error: Value not dropped.
}
This is to ensure the soundness of Cairo programs. Soundness refers to the fact that if a
statement during the execution of the program is false, no cheating prover can convince an
honest verifier that it is true. In our case, we want to ensure the consistency of
consecutive dictionary key updates during program execution, which is only checked when
the dictionaries aresquashed
- which moves the ownership of the dictionary to the
squash
method, thus allowing the dictionary to go out of scope. Unsquashed dictionaries
are dangerous, as a malicious prover could prove the correctness of inconsistent updates.
However, types that implement the Drop
trait are allowed to go out of scope without being explicitly moved. When a value of a type that implements the Drop
trait goes out of scope, the Drop
implementation is called on the type, which moves the value to the drop
function, allowing it to go out of scope - This is what we call "dropping" a value.
It is important to note that the implementation of drop is a "no-op", meaning that it doesn't perform any actions other than allowing the value to go out of scope.
The Drop
implementation can be derived for all types, allowing them to be dropped when goint out of scope, except for dictionaries (Felt252Dict
) and types containing dictionaries.
For example, the following code compiles:
#[derive(Drop)]
struct A {}
fn main() {
A {}; // Now there is no error.
}
The Destruct
Trait
Manually calling the squash
method on a dictionary is not very convenient, and it is easy to forget to do so. To make it easier to use dictionaries, Cairo provides the Destruct
trait, which allows you to specify the behavior of a type when it goes out of scope. While Dictionaries don't implement the Drop
trait, they do implement the Destruct
trait, which allows them to automatically be squashed
when they go out of scope. This means that you can use dictionaries without having to manually call the squash
method.
Consider the following example, in which we define a custom type that contains a dictionary:
use dict::Felt252DictTrait;
struct A {
dict: Felt252Dict<u128>
}
fn main() {
A {
dict: Felt252DictTrait::new()
};
}
If you try to run this code, you will get a compile-time error:
error: Variable not dropped. Trait has no implementation in context: core::traits::Drop::<temp7::temp7::A>. Trait has no implementation in context: core::traits::Destruct::<temp7::temp7::A>.
--> temp7.cairo:7:5
A {
^*^
When A goes out of scope, it can't be dropped as it implements neither the Drop
(as it contains a dictionary and can't derive(Drop)
) nor the Destruct
trait. To fix this, we can derive the Destruct
trait implementation for the A
type:
use dict::Felt252DictTrait;
#[derive(Destruct)]
struct A {
dict: Felt252Dict<u128>
}
fn main() {
A {
dict: Felt252DictTrait::new()
}; // No error here
}
Now, when A
goes out of scope, its dictionary will be automatically squashed
, and the program will compile.
Copy Array data with Clone
If we do want to deeply copy the data of an Array
, we can use a common method called clone
. We’ll discuss method syntax in Chapter 5, but because methods are a common feature in many
programming languages, you’ve probably seen them before.
Here’s an example of the clone
method in action.
Note: in the following example, we need to import the
Clone
trait from the corelibclone
module, and its implementation for the array type from thearray
module.
use array::ArrayTrait;
use clone::Clone;
use array::ArrayTCloneImpl;
...
let arr1 = ArrayTrait::<u128>::new();
let arr2 = arr1.clone();
Note: you will need to run
cairo-run
with the--available-gas=2000000
option to run this example, because it uses a loop and must be ran with a gas limit.
When you see a call to clone
, you know that some arbitrary code is being
executed and that code may be expensive. It’s a visual indicator that something
different is going on.
Ownership and Functions
Passing a variable to a function will either move it or copy it. As seen in the Array section, passing an Array
as a function parameter transfers its ownership; let's see what happens with other types.
Listing 3-3 has an example with some annotations showing where variables go into and out of scope.
Filename: src/main.cairo#[derive(Drop)]
struct MyStruct{}
fn main() {
let my_struct = MyStruct{}; // my_struct comes into scope
takes_ownership(my_struct); // my_struct's value moves into the function...
// ... and so is no longer valid here
let x = 5_u128; // x comes into scope
makes_copy(x); // x would move into the function,
// but u128 implements Copy, so it is okay to still
// use x afterward
} // Here, x goes out of scope and is dropped.
fn takes_ownership(some_struct: MyStruct) { // some_struct comes into scope
} // Here, some_struct goes out of scope and `drop` is called.
fn makes_copy(some_uinteger: u128) { // some_uinteger comes into scope
} // Here, some_integer goes out of scope and is dropped.
If we tried to use my_struct
after the call to takes_ownership
, Cairo would throw a
compile-time error. These static checks protect us from mistakes. Try adding
code to main
that uses my_struct
and x
to see where you can use them and where
the ownership rules prevent you from doing so.
Return Values and Scope
Returning values can also transfer ownership. Listing 3-4 shows an example of a function that returns some value, with similar annotations as those in Listing 4-3.
Filename: src/main.cairo#[derive(Drop)]
struct A{}
fn main() {
let a1 = gives_ownership(); // gives_ownership moves its return
// value into a1
let a2 = A{}; // a2 comes into scope
let a3 = takes_and_gives_back(a2); // a2 is moved into
// takes_and_gives_back, which also
// moves its return value into a3
} // Here, a3 goes out of scope and is dropped. a2 was moved, so nothing
// happens. a1 goes out of scope and is dropped.
fn gives_ownership() -> A { // gives_ownership will move its
// return value into the function
// that calls it
let some_a = A{}; // some_a comes into scope
some_a // some_a is returned and
// moves ownership to the calling
// function
}
// This function takes an instance some_a of A and returns it
fn takes_and_gives_back(some_a: A) -> A { // some_a comes into
// scope
some_a // some_a is returned and moves
// ownership to the calling
// function
}
When a variable goes out of scope, its value is dropped, unless ownership of the value has been moved to another variable.
While this works, taking ownership and then returning ownership with every function is a bit tedious. What if we want to let a function use a value but not take ownership? It’s quite annoying that anything we pass in also needs to be passed back if we want to use it again, in addition to any data resulting from the body of the function that we might want to return as well.
Cairo does let us return multiple values using a tuple, as shown in Listing 3-5.
Filename: src/main.cairouse array::ArrayTrait;
fn main() {
let arr1 = ArrayTrait::<u128>::new();
let (arr2, len) = calculate_length(arr1);
}
fn calculate_length(arr: Array<u128>) -> (Array<u128>, usize) {
let length = arr.len(); // len() returns the length of an array
(arr, length)
}
But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Cairo has two features for using a value without transferring ownership, called references and snapshots.