Cario’s Reassigned and Revoked Reference Example

The example code found on this section of the Cairo docs helps us understand why references created with the let keyword are fundamentally different to values created with the tempvar and local keywords and why references are sometimes revoked when performing jumps.

func foo(x):
    let y = 1
    jmp x_not_zero if x != 0

    x_is_zero:
    [ap] = y; ap++  # y == 1.
    let y = 2
    [ap] = y; ap++  # y == 2.
    jmp done

    x_not_zero:
    [ap] = y; ap++  # y == 1.
    let y = 3
    [ap] = y; ap++  # y == 3.

    done:
    # Here, y is revoked, and cannot be accessed.
    ret
end

For the example code to work, we need to define a main function where we call foo passing a value for x.

func foo(x):
    # same code as before
end

func main():
    foo(5)
    ret
end

In the example we are told that the code compiles and its output is either [1, 2] or [1, 3] depending on the value of x. We can verify this by compiling, executing and analyzing the memory usage of our example code.

$ cairo-compile example_code.cairo --output example_code.json
$ cairo-run --program=example_code.json --print_memory --relocate_prints
>>>
Addr  Value
-----------
⋮
1     146226256843603965
2     8
3     5189976364521848832
4     1
5     5189976364521848832
6     2
7     74168662805676031
8     6
9     5189976364521848832
10    1
11    5189976364521848832
12    3
13    2345108766317314046
14    5189976364521848832
15    5
16    1226245742482522112
17    -15
18    2345108766317314046
19    26
20    26
21    5
22    21
23    18
24    1
25    3

Sure enough, for x = 5 we get the output [1, 3].

The output is surprising for two reasons:

  • If Cairo’s memory is immutable, why was it possible to reassign the value of y?
  • If jumps cause references to be revoked, why is the reference y not revoked in the initial jump but it is revoked after the done label?

Resolving References at Compile Time

Using the let keyword to store a value doesn’t require the use of the ap or fp pointers for storage as tempvar and local would. let is resolved at compile time using only static analysis. We could in fact take the role of the compiler and manually resolve the let keyword in our code the same way the compiler would.

func foo(x):
    jmp x_not_zero if x != 0

    x_is_zero:
    [ap] = 1; ap++
    [ap] = 2; ap++
    jmp done

    x_not_zero:
    [ap] = 1; ap++
    [ap] = 3; ap++

    done:
    ret
end

func main():
    foo(5)
    ret
end

Compiling and executing this code will give us exactly the same output as before, confirming that both programs are equivalent.

Addr  Value
-----------
⋮
1     146226256843603965
2     8
3     5189976364521848832
4     1
5     5189976364521848832
6     2
7     74168662805676031
8     6
9     5189976364521848832
10    1
11    5189976364521848832
12    3
13    2345108766317314046
14    5189976364521848832
15    5
16    1226245742482522112
17    -15
18    2345108766317314046
19    26
20    26
21    5
22    21
23    18
24    1
25    3

In short, we are able to reassign the value of y because it was created using the let keyword which is resolved by the compiler without using the ap of fp pointers for storage. The let keyword creates a reference resolved by static analysis, not a pointer to a memory address.

Revoked References

The example code tells us that the y reference gets revoked after the done label, so let’s verify this claim.

func foo(x):
    let y = 1
    jmp x_not_zero if x != 0

    x_is_zero:
    [ap] = y; ap++
    let y = 2
    [ap] = y; ap++
    jmp done

    x_not_zero:
    [ap] = y; ap++
    let y = 3
    [ap] = y; ap++

    done:
    [ap] = y; ap++
    ret
end

func main():
    foo(5)
    ret
end

Let’s try now to compile this code:

$ cairo-compile example_code.cairo --output example_code.json
>>>
example_code.cairo:17:12: Reference 'y' was revoked.
    [ap] = y; ap++

In effect, the y reference is revoked at this point of the code, but why? If we try to take the role of the compiler and manually replace the y reference we can see the issue more clearly.

func foo(x):
    jmp x_not_zero if x != 0

    x_is_zero:
    [ap] = 1; ap++
    [ap] = 2; ap++
    jmp done

    x_not_zero:
    [ap] = 1; ap++
    [ap] = 3; ap++

    done:
    [ap] = ? # is it 2 or 3?
    ret
end

Performing only static analysis it is impossible to know what the value of that last assignment would be as the value of x is not known in the context of the foo function. This is why the reference is revoked, there is more than one possible value for y at that point in the code.

So, what do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.