Arrays¶
Basic properties¶
In Guppy, an array is an ordered collection of objects of the same type, with a size that is fixed and known at compile time. These two properties distinguish arrays from Python lists.
Arrays are mutable: their values can be reassigned at runtime.
An array can be created using the array constructor. The type signature is array[T, n] where T is the type of the data and n is the size of the array.
from guppylang import guppy
from guppylang.std.builtins import array
@guppy
def get_array() -> array[int, 3]:
return array(0, 2, 4)
Note that in Guppy it is necessary to annotate both the type and the size of an array in function signatures.
Array entries can be changed as follows:
@guppy
def mutate_array() -> array[int, 3]:
numbers = get_array() # Create array containing 0, 2 and 4
numbers[0] = 17 # Change first element to 17
return numbers # Return modified array
mutate_array.check()
Arrays can also be nested, meaning that the elements of an array can themselves be arrays.
@guppy
def get_array_of_arrays() -> array[array[int, 4], 3]:
return array(array(1, 2, 3, 4), array(2, 4, 6, 8), array(3, 6, 9, 12))
get_array_of_arrays.check()
Frozenarrays¶
Note that in addition to the standard array type, there is also frozenarray which is immutable.
Currently frozenarrays can only be created when loading a Python list in a comptime or py expression. For more on comptime expressions, see the relevant language guide section.
As frozenarray is immutable we cannot reassign its entries as we can with the array type.
from guppylang.std.array import frozenarray
from guppylang.std.builtins import comptime
@guppy
def mutate_frozenarray() -> frozenarray[int, 5]:
numbers = comptime([1, 3, 5, 7, 9]) # Create a frozenarray from a Python list
numbers[0] = 39 # Try to change first element to 39
return numbers
mutate_frozenarray.check()
Error: Unsupported (at <In[4]>:7:4)
|
5 | def mutate_frozenarray() -> frozenarray[int, 5]:
6 | numbers = comptime([1, 3, 5, 7, 9]) # Create a frozenarray from a Python list
7 | numbers[0] = 39 # Try to change first element to 39
| ^^^^^^^^^^ Subscript assignments to non-arrays are not supported
Guppy compilation failed due to 1 previous error
Note that it is preferable to use frozenarray (rather than a mutable array) where possible for performance reasons. Being immutable, a frozenarray will compile faster and have superior runtime performance when targeting Quantinuum systems hardware and emulators.
An example use case for a frozenarray would be for lookup table decoders in quantum error correction. For example, we could precompute a large numpy array of integers which represent syndromes and their corresponding corresponding corrections. This array can then be loaded into a Guppy context as a comptime list. We can then have read only access to our table during the runtime of our quantum program.
Indexing into arrays¶
As in Python, Guppy indices start from zero. In the array arr = array(0, 2, 4) we can access the element 0 with arr[0], 4 with arr[2], and so on.
Warning
Although the size of an array is known at compile time, the index may not be. If an index computed at runtime is out of bounds, a runtime error will occur.
If our index is an integer literal, the Guppy compiler can detect when the index is out of bounds and give an error.
from guppylang.std.quantum import h, qubit
@guppy
def index_out_of_bounds1() -> array[qubit, 3]:
qs = array(qubit() for _ in range(3)) # Allocate an array of length 3
h(qs[3]) # Access index 3, only (0, 1, 2) indices are within bounds
return qs
index_out_of_bounds1.check() # Out of bounds error given
Error: Index out of bounds (at <In[5]>:6:9)
|
4 | def index_out_of_bounds1() -> array[qubit, 3]:
5 | qs = array(qubit() for _ in range(3)) # Allocate an array of length 3
6 | h(qs[3]) # Access index 3, only (0, 1, 2) indices are within bounds
| ^ Array index 3 is out of bounds for array of size 3.
Guppy compilation failed due to 1 previous error
Note that there are some limitations to this bounds checking. If we write the index as an expression i.e. qs[2+1] then the compiler is not able to detect that the index is out of bounds. Also if we assign the value 3 to a variable x then qs[x] will pass the type check.
@guppy
def index_out_of_bounds2() -> array[qubit, 3]:
qs = array(qubit() for _ in range(3)) # Allocate an array of length 3
x = 3 # Assign 3 to a variable
h(qs[x]) # Index using the variable x
h(qs[1 + 2]) # Index is an arithmetic expression
return qs
index_out_of_bounds2.check() # No out of bounds error given
Array comprehensions¶
We can use array comprehension to create an array object without specifying all of its elements individually. This is especially useful for dealing with large arrays.
Syntactically, Guppy comprehensions are similar to list comprehensions in Python.
@guppy
def get_first_four_squares() -> array[int, 4]:
return array(x*x for x in range(4))
get_first_four_squares.check()
Note that as the size of an array has to be statically known we cannot generalize this function using a generic variable.
from guppylang.std.num import nat
n = guppy.nat_var("n")
@guppy
def get_first_n_squares(n: nat) -> array[int, n]:
return array(x*x for x in range(n))
get_first_n_squares.check()
Error: Array comprehension with nonstatic size (at <In[8]>:7:16)
|
5 | @guppy
6 | def get_first_n_squares(n: nat) -> array[int, n]:
7 | return array(x*x for x in range(n))
| ^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer the size of this array comprehension ...
Note:
|
6 | def get_first_n_squares(n: nat) -> array[int, n]:
7 | return array(x*x for x in range(n))
| -------- since the number of elements yielded by this iterator is not
| statically known
Guppy compilation failed due to 1 previous error
Note
Note that we can generalize this function provided that the value of n is known at compile time.
See the section on comptime arguments.
For more background on Guppy’s static type checker see the section on Static Compilation and Typing.
We cannot use conditional statements in array comprehensions as their values generally can’t be known at compile time.
@guppy
def filter_squares_by_divisor(divisor: int) -> array[int, 3]:
squares = get_first_four_squares()
return array(x for x in squares if x % divisor == 0) # Size cannot be determined statically
filter_squares_by_divisor.check()
Error: Array comprehension with nonstatic size (at <In[9]>:4:16)
|
2 | def filter_squares_by_divisor(divisor: int) -> array[int, 3]:
3 | squares = get_first_four_squares()
4 | return array(x for x in squares if x % divisor == 0) # Size cannot be determined statically
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer the size of this array comprehension ...
Note:
|
3 | squares = get_first_four_squares()
4 | return array(x for x in squares if x % divisor == 0) # Size cannot be determined statically
| ---------------- since it depends on this condition
Guppy compilation failed due to 1 previous error
Array unpacking¶
The elements within a Guppy array can be accessed via unpacking similarly to Python tuples. To see how unpacking works for Guppy tuples see the tuple unpacking section.
We can use the * operator to unpack multiple elements.
@guppy
def make_array() -> array[int, 4]:
return array(5, 10, 15, 20)
@guppy
def unpack_tail(arr: array[int, 4]) -> tuple[int, array[int, 3]]:
first, *tail = make_array()
return first, tail
unpack_tail.check()
A current limitation of array unpacking is that it is not supported for arrays of generic length.
Note
Note that it in Guppy it is possible to unpack any iterable type.
For example we can unpack a Range as follows first, *tail = range(10).
Moving and copying arrays¶
Guppy arrays are affine, meaning their value can be used once or not at all. Assignment of arrays does not copy their values into a new array, but just moves the reference.
@guppy
def make_big_array() -> array[int, 96]:
return array(x*x for x in range(96))
@guppy
def main() -> None:
arr1 = make_big_array()
arr2 = arr1 # Move the value arr1 to arr2
arr1[1] = 17 # Compiler error, arr1 cannot be indexed into after the move
main.check()
Error: Copy violation (at <In[11]>:9:4)
|
7 | arr1 = make_big_array()
8 | arr2 = arr1 # Move the value arr1 to arr2
9 | arr1[1] = 17 # Compiler error, arr1 cannot be indexed into after the move
| ^^^^^^^ Variable `arr1` with non-copyable type `array[int, 96]`
| cannot be borrowed ...
Note:
|
7 | arr1 = make_big_array()
8 | arr2 = arr1 # Move the value arr1 to arr2
| ---- Variable `arr1` already moved here
Help: Consider copying `arr1` instead of moving it: `arr1.copy()`
Guppy compilation failed due to 1 previous error
Assignment of an array to the new arr2 variable moves the value of arr1 to arr2. The value of arr1 cannot be used after it is moved.
Arrays can still be copied explicitly using the array.copy() method if they contain objects with a copyable type.
@guppy
def main() -> None:
arr1 = make_big_array()
arr2 = arr1.copy() # Explicitly copy arr1 and assign to arr2
arr1[95] = 419 # arr1 can still be used as it hasn't been moved
main.check()
Explicit copying is a design choice with performance implications. Arrays can be large, and copying can be a significant memory overhead.
Array copying therefore has to be explicitly opted into via the array.copy() method rather than done implicitly with variable assignment.
Note that arrays cannot be copied after a move.
@guppy
def main() -> None:
arr1 = make_big_array()
arr2 = arr1 # Move the value arr1 to arr2
arr3 = arr1.copy() # Compiler error
main.check()
Error: Copy violation (at <In[13]>:5:11)
|
3 | arr1 = make_big_array()
4 | arr2 = arr1 # Move the value arr1 to arr2
5 | arr3 = arr1.copy() # Compiler error
| ^^^^ Variable `arr1` with non-copyable type `array[int, 96]`
| cannot be borrowed ...
Note:
|
3 | arr1 = make_big_array()
4 | arr2 = arr1 # Move the value arr1 to arr2
| ---- Variable `arr1` already moved here
Help: Consider copying `arr1` instead of moving it: `arr1.copy()`
Guppy compilation failed due to 1 previous error
Arrays of non-copyable types, such as qubits, cannot be copied. Also if an array contains qubits, it cannot be implicitly discarded.
It must be discarded explicitly with the discard_array function to avoid violating linearity.
Nested arrays cannot be copied directly. A two-dimensional array can be copied via comprehension as follows.
@guppy
def make_2d_array() -> array[array[int, 3], 3]:
return array(array(1, 2, 3), array(1, 4, 9), array(1, 8, 27))
@guppy
def main() -> None:
arr = make_2d_array()
# arr.copy() # would give a compiler error
copied_arr = array(inner.copy() for inner in arr)
copied_arr[1][1] = 31
main.check()
Note that for loops currently take ownership of the iterable, which is useful to keep in mind when you are iterating directly over arrays as opposed to using subscripts:
from guppylang.std.builtins import owned
m = guppy.nat_var("m")
@guppy
def f(x: int) -> None:
pass
@guppy
def apply_f(xs: array[int, m] @owned) -> array[int, m]:
for x in xs:
f(x)
return xs
apply_f.check()
Error: Copy violation (at <In[15]>:13:11)
|
11 | for x in xs:
12 | f(x)
13 | return xs
| ^^ Variable `xs` with non-copyable type `array[int, m]` cannot
| be returned ...
Note:
|
10 | def apply_f(xs: array[int, m] @owned) -> array[int, m]:
11 | for x in xs:
| -- Variable `xs` already consumed here
Help: Consider copying `xs` instead of moving it: `xs.copy()`
Guppy compilation failed due to 1 previous error
Explicit copying can come in handy here, if it is possible to do with the array that is being iterated over:
@guppy
def apply_f(xs: array[int, m] @owned) -> array[int, m]:
for x in xs.copy():
f(x)
return xs
apply_f.check()
A frozenarray can be copied with the frozenarray.mutable_copy method.
from guppylang.std.array import frozenarray
from guppylang.std.builtins import comptime
@guppy
def main() -> None:
# Create a frozenarray using a comptime expression
frozen_arr = comptime([1, 11, 21])
# Copy the frozenarray
arr_copy: array[int, 3] = frozen_arr.mutable_copy()
# The arr_copy object is mutable
arr_copy[0] = 171
main.check()
Note that the return type of frozenarray.mutable_copy is of type array.
Example usage of arrays¶
To see some uses of arrays in practice, refer to the following examples: