Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions guppylang/src/guppylang/std/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,11 @@ def __new__(cls, *args: T) -> builtins.list[T]: # type: ignore[no-redef]
@guppy
@no_type_check
def reverse_in_place(self) -> None:
"""Reverse array elements in-place.
"""Reverse array elements in place.

Takes O(n) time and O(1) auxiliary space, supporting linear elements.
Borrows the array and supports linear elements. Empty and singleton
arrays are unchanged. Pairwise exchanges take O(n) time and O(1)
auxiliary space.

.. code-block:: python

Expand All @@ -387,6 +389,85 @@ def reverse_in_place(self) -> None:
for i in range(n // 2):
mem_swap(self[i], self[n - i - 1])

@guppy
@no_type_check
def swap(self, first: int, second: int) -> None:
"""Swap two array elements in place using `array_swap`.

Borrows the array and supports linear elements. Equal valid indices
leave it unchanged. Panics if either index is negative or out of bounds.
Takes O(1) time and auxiliary space.

.. code-block:: python

values = array(1, 2, 3)
values.swap(0, 2) # [3, 2, 1]
"""
array_swap(self, first, second)

@guppy
@no_type_check
def fill[T: (Copy, Drop), n: nat](self: array[T, n], value: T) -> None:
"""Replace every element with a copy of value, borrowing both inputs.

Requires Copy and Drop to copy the replacement and discard old elements.
Empty arrays are unchanged. A single scan takes O(n) time and O(1)
auxiliary space.

.. code-block:: python

values = array(1, 2, 3)
values.fill(7) # [7, 7, 7]
"""
for i in range(n):
self[i] = value

@guppy
@no_type_check
def rotate_left_in_place(self, amount: int) -> None:
"""Rotate elements left by amount, preserving their cyclic order.

Borrows the array and supports linear elements. Amounts wrap modulo
the length; negative amounts rotate right. Empty arrays are unchanged
for any amount. Zero effective rotations are no-ops.
The rotation is implemented using three reversals, taking O(n) time
and O(1) auxiliary space.

.. code-block:: python

values = array(1, 2, 3, 4)
values.rotate_left_in_place(1) # [2, 3, 4, 1]
"""
amount = _normalize_rotation(n, amount)
if amount == 0:
return
_reverse_range(self, 0, amount)
_reverse_range(self, amount, n)
self.reverse_in_place()

@guppy
@no_type_check
def rotate_right_in_place(self, amount: int) -> None:
"""Rotate elements right by amount, preserving their cyclic order.

Borrows the array and supports linear elements. Amounts wrap modulo
the length; negative amounts rotate left. Empty arrays are unchanged
for any amount. Zero effective rotations are no-ops.
The rotation is implemented using three reversals, taking O(n) time
and O(1) auxiliary space.

.. code-block:: python

values = array(1, 2, 3, 4)
values.rotate_right_in_place(1) # [4, 1, 2, 3]
"""
amount = _normalize_rotation(n, amount)
if amount == 0:
return
self.reverse_in_place()
_reverse_range(self, 0, amount)
_reverse_range(self, amount, n)

@guppy
@no_type_check
def concat[T, n: nat, m: nat](
Expand Down Expand Up @@ -933,6 +1014,26 @@ def to_nat_le[n: nat](self: array[bool, n]) -> nat:
return value


@guppy
@no_type_check
def _normalize_rotation(length: int, amount: int) -> int:
if length == 0:
return 0
# Normalize the signed remainder without negating a potentially minimal int.
offset = amount % length
if offset < 0:
offset += length
return offset


@guppy
@no_type_check
def _reverse_range[T, n: nat](values: array[T, n], start: int, stop: int) -> None:
# Callers validate the half-open range before the first exchange.
for i in range((stop - start) // 2):
array_swap(values, start + i, stop - i - 1)


@guppy
@no_type_check
def _unzip_first[T, U, n: nat](
Expand Down Expand Up @@ -1047,7 +1148,8 @@ def array_swap[L, n: nat](arr: array[L, n], idx: int, idx2: int) -> None:

The swap happens in-place. Panics if either index is out of bounds.

Works with both copyable and linear types (like qubits).
Works with both copyable and linear types (like qubits). Equal valid indices
leave the array unchanged. Takes O(1) time and auxiliary space.

Args:
arr: The array to modify
Expand Down
178 changes: 178 additions & 0 deletions tests/integration/test_array_mutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""In-place updates and permutations preserve complete arrays and linear elements."""

from typing import no_type_check

import pytest

from guppylang import guppy
from guppylang.emulator import EmulatorError
from guppylang.std.array import array, array_swap
from guppylang.std.num import nat
from guppylang.std.platform import output
from guppylang.std.quantum import measure, qubit, x


@guppy
@no_type_check
def _emit[n: nat](values: array[int, n]) -> None:
if n:
output("values", values)
else:
# The output backend rejects empty arrays.
output("length", len(values))


@no_type_check
def test_array_mutations() -> None:
cases = [[], [7], [1, 2, 3, 4], [3, 1, 4, 2, 5]]
amounts = [-1, 0, 1]

@guppy
def check[n: nat](xs: array[int, n]) -> None:
if n:
swapped = xs.copy()
swapped.swap(0, n - 1)
_emit(swapped)
swapped.swap(0, 0)
_emit(swapped)
primitive = xs.copy()
array_swap(primitive, 0, n - 1)
_emit(primitive)
array_swap(primitive, 0, 0)
_emit(primitive)
filled = xs.copy()
filled.fill(-7)
_emit(filled)
reversed_values = xs.copy()
reversed_values.reverse_in_place()
_emit(reversed_values)
for amount in amounts:
left = xs.copy()
left.rotate_left_in_place(amount)
_emit(left)
right = xs.copy()
right.rotate_right_in_place(amount)
_emit(right)
_emit(xs)

@guppy
def empty() -> None:
check(array())

@guppy
def flags() -> None:
xs = array(False, True)
xs.fill(True)
output("flags", xs)

@guppy.comptime
def main() -> None:
empty()
for values in cases[1:]:
check(list(values))
flags()

expected = []
for values in cases:
size = len(values)
if values:
swapped = values.copy()
swapped[0], swapped[-1] = swapped[-1], swapped[0]
# Each swap API exchanges endpoints, then swaps an index with itself.
# The second call leaves the endpoint-swapped result unchanged.
expected.extend([("values", swapped)] * 4)

# The helper emits fill, reverse, paired left/right rotations, then xs.
results = [[-7] * size, list(reversed(values))]
# One offset of each sign plus zero; integer limits are checked separately.
for amount in amounts:
offset = amount % size if values else 0
# Left moves the prefix to the end; right moves the suffix to the front.
left = values[offset:] + values[:offset]
right_start = size - offset
right = values[right_start:] + values[:right_start]
results.extend([left, right])
results.append(values) # All operations acted on copies; xs is unchanged.
expected.extend(
("values", result) if result else ("length", 0) for result in results
)
expected.append(("flags", [True, True]))
assert main.emulator(0).coinflip_sim().run()[0].entries == expected


@no_type_check
def test_linear_permutations() -> None:
@guppy
def main() -> None:
# An asymmetric preparation distinguishes the composed permutation.
qs = array(qubit() for _ in range(5)) # 00000
x(qs[0]) # 10000
x(qs[1]) # 11000
qs.swap(0, 4) # 01001
qs.swap(2, 2) # 01001
array_swap(qs, 1, 3) # 00011
array_swap(qs, 2, 2) # 00011
qs.reverse_in_place() # 11000
qs.rotate_left_in_place(2) # 00011
qs.rotate_right_in_place(-4) # 10001
for q in qs:
output("measurement", measure(q).read())

assert main.emulator(5).stabilizer_sim().run()[0].entries == [
("measurement", value) for value in [True, False, False, False, True]
]


@no_type_check
def test_rotation_integer_limits() -> None:
amounts = [-(1 << 63), (1 << 63) - 1]

@guppy
def check(amount: int) -> None:
left = array(1, 2, 3)
right = left.copy()
left.rotate_left_in_place(amount)
right.rotate_right_in_place(amount)
output("left", left)
output("right", right)
# Neither direction may divide by zero on an empty array, at any offset.
empty: array[int, 0] = array()
empty.rotate_left_in_place(amount)
empty.rotate_right_in_place(amount)
output("empty", len(empty))

@guppy.comptime
def main() -> None:
for amount in amounts:
check(amount)

expected = []
values = [1, 2, 3]
for amount in amounts:
offset = amount % len(values)
expected.extend(
[
("left", values[offset:] + values[:offset]),
("right", values[-offset:] + values[:-offset] if offset else values),
("empty", 0),
]
)
assert main.emulator(0).coinflip_sim().run()[0].entries == expected


@no_type_check
def test_swap_invalid_index() -> None:
@guppy
def main(first: int, second: int, method: bool) -> None:
xs = array(1, 2)
if method:
xs.swap(first, second)
else:
array_swap(xs, first, second)
output("values", xs)

emulator = main.emulator(0).coinflip_sim()
for first, second in [(-1, 0), (0, -1), (2, 0), (0, 2), (2, 2)]:
for method in [False, True]:
with pytest.raises(EmulatorError, match="Array swap failed"):
emulator.run(first=first, second=second, method=method)
Loading