Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
40d1255
chore: update to hugr-py v0.13
ss2165 Jul 10, 2025
60dccb3
Dev deps
tatiana-s Jul 30, 2025
4054f8f
feat: Insert drop ops for affine values
mark-koch Jul 14, 2025
a1b19af
Fix imports and bound
tatiana-s Jul 30, 2025
169aa76
Progress
tatiana-s Aug 4, 2025
8272f8c
Check
tatiana-s Aug 5, 2025
eeecf53
Small fixes
tatiana-s Aug 5, 2025
b9d961b
More fixes
tatiana-s Aug 6, 2025
3df7013
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Aug 7, 2025
db52f55
Result attempt with clone
tatiana-s Aug 7, 2025
c9fe2d4
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Aug 19, 2025
eedecd8
Fix after merge
tatiana-s Aug 20, 2025
001da68
update wheel
tatiana-s Aug 21, 2025
1926639
Stop inlining
tatiana-s Aug 22, 2025
c70670f
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Sep 1, 2025
71b9fc6
Update uv
tatiana-s Sep 1, 2025
31d92cc
Latest wheel
tatiana-s Sep 3, 2025
1fa119e
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Sep 24, 2025
8c87895
Fix merge issue
tatiana-s Sep 24, 2025
b75047d
Fix array assert iter (and update wheel)
tatiana-s Oct 1, 2025
ab08667
More fixes
tatiana-s Oct 1, 2025
7575884
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Oct 1, 2025
7342398
Fix types
tatiana-s Oct 1, 2025
b8a33c3
Fix copy
tatiana-s Oct 3, 2025
9a63367
Adjust to comments
tatiana-s Oct 14, 2025
5525ea5
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
tatiana-s Oct 14, 2025
e81872d
Fix inout order
tatiana-s Oct 15, 2025
dda2742
Use get in classical array getitem now that there is a fix for bool l…
tatiana-s Oct 17, 2025
025880d
chore: update tket-exts dep
ss2165 Oct 20, 2025
60665ba
released tket
ss2165 Oct 20, 2025
d5b4ca6
remove qis-compiler hard dependency
ss2165 Oct 20, 2025
9332015
add qis-compiler dependency to internals
ss2165 Oct 21, 2025
f1fc799
comments/typos
acl-cqc Oct 21, 2025
3635907
Verify that whenever is_affine is read, it's (not copy and drop) - br…
acl-cqc Oct 21, 2025
982d8a9
Revert "Verify that whenever is_affine is read, it's (not copy and dr…
acl-cqc Oct 21, 2025
5f3e366
released qis-compiler
ss2165 Oct 21, 2025
a6d3d0b
extend test_drop_bound to demonstrate failure
acl-cqc Oct 21, 2025
f38682c
Remove test, comment about bug
acl-cqc Oct 21, 2025
c1c0808
Merge remote-tracking branch 'origin/main' into ts/borrow-arrays
acl-cqc Oct 21, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from hugr.hugr.node_port import ToNode
from hugr.std import PRELUDE
from hugr.std.collections.array import EXTENSION as ARRAY_EXTENSION
from hugr.std.collections.borrow_array import EXTENSION as BORROW_ARRAY_EXTENSION
from typing_extensions import assert_never

from guppylang_internals.checker.core import (
Expand Down Expand Up @@ -247,7 +248,7 @@ def compile(self, defn: CheckedDef) -> CompiledDef:

# Insert explicit drops for affine types
# TODO: This is a quick workaround until we can properly insert these drops
# during linearity checking. See https://github.com/CQCL/guppylang/issues/1082
# during linearity checking. See https://github.com/CQCL/guppylang/issues/1082
insert_drops(self.module.hugr)

return entry_compiled
Expand Down Expand Up @@ -651,6 +652,7 @@ def qualified_name(type_def: he.TypeDef) -> str:
#: insertion of an explicit drop operation.
AFFINE_EXTENSION_TYS: list[str] = [
qualified_name(ARRAY_EXTENSION.get_type("array")),
qualified_name(BORROW_ARRAY_EXTENSION.get_type("borrow_array")),
]


Expand Down Expand Up @@ -698,7 +700,7 @@ def insert_drops(hugr: Hugr[OpVarCov]) -> None:
# Iterating over `node.outputs()` doesn't work reliably since it sometimes
# raises an `IncompleteOp` exception. Instead, we query the number of out ports
# and look them up by index. However, this method is *also* broken when
# isnpecting `FuncDefn` nodes due to https://github.com/CQCL/hugr/issues/2438.
# inspecting `FuncDefn` nodes due to https://github.com/CQCL/hugr/issues/2438.
if isinstance(data.op, ops.FuncDefn):
continue
for i in range(hugr.num_out_ports(node)):
Expand Down
156 changes: 45 additions & 111 deletions guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,17 @@
from guppylang_internals.std._internal.compiler.arithmetic import (
UnsignedIntVal,
convert_ifromusize,
convert_itousize,
)
from guppylang_internals.std._internal.compiler.array import (
array_convert_from_std_array,
array_convert_to_std_array,
array_clone,
array_map,
array_new,
array_repeat,
array_to_std_array,
barray_new_all_borrowed,
barray_return,
standard_array_type,
std_array_to_array,
unpack_array,
)
from guppylang_internals.std._internal.compiler.list import (
Expand All @@ -79,7 +82,6 @@
from guppylang_internals.std._internal.compiler.prelude import (
build_error,
build_panic,
build_unwrap,
panic,
)
from guppylang_internals.std._internal.compiler.tket_bool import (
Expand All @@ -91,6 +93,7 @@
)
from guppylang_internals.tys.arg import ConstArg
from guppylang_internals.tys.builtin import (
array_type,
bool_type,
get_element_type,
int_type,
Expand Down Expand Up @@ -556,25 +559,32 @@ def visit_ResultExpr(self, node: ResultExpr) -> Wire:
op_name = f"result_array_{base_name}"
size_arg = node.array_len.to_arg().to_hugr(self.ctx)
extra_args = [size_arg, *extra_args]
# Remove the option wrapping in the array
unwrap = array_unwrap_elem(self.ctx)
unwrap = self.builder.load_function(
unwrap,
instantiation=ht.FunctionType([ht.Option(base_ty)], [base_ty]),
type_args=[ht.TypeTypeArg(base_ty)],
# As `borrow_array`s used by Guppy are linear, we need to clone it (knowing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# As `borrow_array`s used by Guppy are linear, we need to clone it (knowing
# As `borrow_array`s are linear, we need to clone it (the elements
# must be copyable as they are output as results) to pass one copy
# to the result operation and return the other

what are the chances of defining return as a pass-through operation, i.e. such that it returns the array? (much like result in Rust would only borrow the array not consume it)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would require the signature of the result which is a breaking change in the extension which was a subject of much discussion and though it was implemented it was then reverted again so I don't think we are doing that for now

# that all elements in it are copyable) to avoid linearity violations when
# both passing it to the result operation and returning it (as an inout
# argument).
value_wire, inout_wire = self.builder.add_op(
array_clone(base_ty, size_arg), value_wire
)
map_op = array_map(ht.Option(base_ty), size_arg, base_ty)
value_wire = self.builder.add_op(map_op, value_wire, unwrap)
func_ty = FunctionType(
[
FuncInput(
array_type(node.base_ty, node.array_len), InputFlags.Inout
),
],
NoneType(),
)
self._update_inout_ports(node.args, iter([inout_wire]), func_ty)
if is_bool_type(node.base_ty):
# We need to coerce a read on all the array elements if they are bools.
array_read = array_read_bool(self.ctx)
array_read = self.builder.load_function(array_read)
map_op = array_map(OpaqueBool, size_arg, ht.Bool)
value_wire = self.builder.add_op(map_op, value_wire, array_read)
base_ty = ht.Bool
# Turn `value_array` into regular linear `array`
# Turn `borrow_array` into regular `array`
value_wire = self.builder.add_op(
array_convert_to_std_array(base_ty, size_arg), value_wire
array_to_std_array(base_ty, size_arg), value_wire
)
hugr_ty: ht.Type = hugr.std.collections.array.Array(base_ty, size_arg)
else:
Expand Down Expand Up @@ -636,20 +646,20 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire:
qubit_arr_in = self.builder.add_op(
array_new(ht.Qubit, len(node.args) - 1), *qubits_in
)
# Turn into standard array from value array.
# Turn into standard array from borrow array.
qubit_arr_in = self.builder.add_op(
array_convert_to_std_array(ht.Qubit, num_qubits_arg), qubit_arr_in
array_to_std_array(ht.Qubit, num_qubits_arg), qubit_arr_in
)

qubit_arr_out = self.builder.add_op(op, qubit_arr_in)

qubit_arr_out = self.builder.add_op(
array_convert_from_std_array(ht.Qubit, num_qubits_arg), qubit_arr_out
std_array_to_array(ht.Qubit, num_qubits_arg), qubit_arr_out
)
qubits_out = unpack_array(self.builder, qubit_arr_out)
else:
# If the input is an array of qubits, we need to unwrap the elements first,
# and then convert to a value array and back.
# If the input is an array of qubits, we need to convert to a standard
# array.
qubits_in = [self.visit(node.args[1])]
qubits_out = [
apply_array_op_with_conversions(
Expand Down Expand Up @@ -681,26 +691,23 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire:
assert isinstance(array_ty, OpaqueType)
array_var = Variable(next(tmp_vars), array_ty, node)
count_var = Variable(next(tmp_vars), int_type(), node)
# See https://github.com/CQCL/guppylang/issues/629
hugr_elt_ty = ht.Option(node.elt_ty.to_hugr(self.ctx))
# Initialise array with `None`s
make_none = array_comprehension_init_func(self.ctx)
make_none = self.builder.load_function(
make_none,
instantiation=ht.FunctionType([], [hugr_elt_ty]),
type_args=[ht.TypeTypeArg(node.elt_ty.to_hugr(self.ctx))],
)
hugr_elt_ty = node.elt_ty.to_hugr(self.ctx)
# Initialise empty array.
self.dfg[array_var] = self.builder.add_op(
array_repeat(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)), make_none
barray_new_all_borrowed(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx))
)
self.dfg[count_var] = self.builder.load(
hugr.std.int.IntVal(0, width=NumericType.INT_WIDTH)
)
with self._build_generators([node.generator], [array_var, count_var]):
elt = self.visit(node.elt)
array, count = self.dfg[array_var], self.dfg[count_var]
[], [self.dfg[array_var]] = self._build_method_call(
array_ty, "__setitem__", node, [array, count, elt], array_ty.args
idx = self.builder.add_op(convert_itousize(), count)
self.dfg[array_var] = self.builder.add_op(
barray_return(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)),
array,
idx,
elt,
)
# Update `count += 1`
one = self.builder.load(hugr.std.int.IntVal(1, width=NumericType.INT_WIDTH))
Expand Down Expand Up @@ -836,10 +843,6 @@ def python_value_to_hugr(v: Any, exp_ty: Type, ctx: CompilerContext) -> hv.Value
return None


ARRAY_COMPREHENSION_INIT: Final[GlobalConstId] = GlobalConstId.fresh(
"array.__comprehension.init"
)

ARRAY_UNWRAP_ELEM: Final[GlobalConstId] = GlobalConstId.fresh("array.__unwrap_elem")
ARRAY_WRAP_ELEM: Final[GlobalConstId] = GlobalConstId.fresh("array.__wrap_elem")

Expand All @@ -849,54 +852,6 @@ def python_value_to_hugr(v: Any, exp_ty: Type, ctx: CompilerContext) -> hv.Value
)


def array_comprehension_init_func(ctx: CompilerContext) -> hf.Function:
"""Returns the Hugr function that is used to initialise arrays elements before a
comprehension.

Just returns the `None` variant of the optional element type.

See https://github.com/CQCL/guppylang/issues/629
"""
v = ht.Variable(0, ht.TypeBound(ht.TypeBound.Linear))
sig = ht.PolyFuncType(
params=[ht.TypeTypeParam(ht.TypeBound.Linear)],
body=ht.FunctionType([], [ht.Option(v)]),
)
func, already_defined = ctx.declare_global_func(ARRAY_COMPREHENSION_INIT, sig)
if not already_defined:
func.set_outputs(func.add_op(ops.Tag(0, ht.Option(v))))
return func


def array_unwrap_elem(ctx: CompilerContext) -> hf.Function:
"""Returns the Hugr function that is used to unwrap the elements in an option array
to turn it into a regular array."""
v = ht.Variable(0, ht.TypeBound(ht.TypeBound.Linear))
sig = ht.PolyFuncType(
params=[ht.TypeTypeParam(ht.TypeBound.Linear)],
body=ht.FunctionType([ht.Option(v)], [v]),
)
func, already_defined = ctx.declare_global_func(ARRAY_UNWRAP_ELEM, sig)
if not already_defined:
msg = "Linear array element has already been used"
func.set_outputs(build_unwrap(func, func.inputs()[0], msg))
return func


def array_wrap_elem(ctx: CompilerContext) -> hf.Function:
"""Returns the Hugr function that is used to wrap the elements in an regular array
to turn it into a option array."""
v = ht.Variable(0, ht.TypeBound(ht.TypeBound.Linear))
sig = ht.PolyFuncType(
params=[ht.TypeTypeParam(ht.TypeBound.Linear)],
body=ht.FunctionType([v], [ht.Option(v)]),
)
func, already_defined = ctx.declare_global_func(ARRAY_WRAP_ELEM, sig)
if not already_defined:
func.set_outputs(func.add_op(ops.Tag(1, ht.Option(v)), func.inputs()[0]))
return func


def array_read_bool(ctx: CompilerContext) -> hf.Function:
"""Returns the Hugr function that is used to unwrap the elements in an option array
to turn it into a regular array."""
Expand Down Expand Up @@ -945,35 +900,21 @@ def apply_array_op_with_conversions(
output array.

Transformations:
1. Unwraps / wraps elements in options.
3. (Optional) Converts from / to opaque bool to / from Hugr bool.
2. Converts from / to value array to / from standard Hugr array.
1. (Optional) Converts from / to opaque bool to / from Hugr bool.
2. Converts from / to borrow array to / from standard Hugr array.
"""
unwrap = array_unwrap_elem(ctx)
unwrap = builder.load_function(
unwrap,
instantiation=ht.FunctionType([ht.Option(elem_ty)], [elem_ty]),
type_args=[ht.TypeTypeArg(elem_ty)],
)
map_op = array_map(ht.Option(elem_ty), size_arg, elem_ty)
unwrapped_array = builder.add_op(map_op, input_array, unwrap)

if convert_bool:
array_read = array_read_bool(ctx)
array_read = builder.load_function(array_read)
map_op = array_map(OpaqueBool, size_arg, ht.Bool)
unwrapped_array = builder.add_op(map_op, unwrapped_array, array_read)
input_array = builder.add_op(map_op, input_array, array_read)
elem_ty = ht.Bool

unwrapped_array = builder.add_op(
array_convert_to_std_array(elem_ty, size_arg), unwrapped_array
)
input_array = builder.add_op(array_to_std_array(elem_ty, size_arg), input_array)

result_array = builder.add_op(op, unwrapped_array)
result_array = builder.add_op(op, input_array)

result_array = builder.add_op(
array_convert_from_std_array(elem_ty, size_arg), result_array
)
result_array = builder.add_op(std_array_to_array(elem_ty, size_arg), result_array)

if convert_bool:
array_make_opaque = array_make_opaque_bool(ctx)
Expand All @@ -982,11 +923,4 @@ def apply_array_op_with_conversions(
result_array = builder.add_op(map_op, result_array, array_make_opaque)
elem_ty = OpaqueBool

wrap = array_wrap_elem(ctx)
wrap = builder.load_function(
wrap,
instantiation=ht.FunctionType([elem_ty], [ht.Option(elem_ty)]),
type_args=[ht.TypeTypeArg(elem_ty)],
)
map_op = array_map(elem_ty, size_arg, ht.Option(elem_ty))
return builder.add_op(map_op, result_array, wrap)
return result_array
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import functools
from collections.abc import Sequence

import hugr.tys as ht
from hugr import Wire, ops
from hugr.build.dfg import DfBase

Expand Down Expand Up @@ -120,8 +119,9 @@ def _assign_tuple(self, lhs: TupleUnpack, port: Wire) -> None:
ports[len(left) : -len(right)] if right else ports[len(left) :]
)
elt = get_element_type(array_ty).to_hugr(self.ctx)
opts = [self.builder.add_op(ops.Some(elt), p) for p in starred_ports]
array = self.builder.add_op(array_new(ht.Option(elt), len(opts)), *opts)
array = self.builder.add_op(
array_new(elt, len(starred_ports)), *starred_ports
)
self._assign(starred, array)

@_assign.register
Expand All @@ -130,7 +130,7 @@ def _assign_array(self, lhs: ArrayUnpack, port: Wire) -> None:
# Given an assignment pattern `left, *starred, right`, pop from the left and
# right, leaving us with the starred array in the middle
length = lhs.length
opt_elt_ty = ht.Option(lhs.elt_type.to_hugr(self.ctx))
elt_ty = lhs.elt_type.to_hugr(self.ctx)

def pop(
array: Wire, length: int, pats: list[ast.expr], from_left: bool
Expand All @@ -141,10 +141,9 @@ def pop(
elts = []
for i in range(num_pats):
res = self.builder.add_op(
array_pop(opt_elt_ty, length - i, from_left), array
array_pop(elt_ty, length - i, from_left), array
)
[elt_opt, array] = build_unwrap(self.builder, res, err)
[elt] = build_unwrap(self.builder, elt_opt, err)
[elt, array] = build_unwrap(self.builder, res, err)
elts.append(elt)
# Assign elements to the given patterns
for pat, elt in zip(
Expand All @@ -164,7 +163,7 @@ def pop(
self._assign(lhs.pattern.starred, array)
else:
assert length == 0
self.builder.add_op(array_discard_empty(opt_elt_ty), array)
self.builder.add_op(array_discard_empty(elt_ty), array)

@_assign.register
def _assign_iterable(self, lhs: IterableUnpack, port: Wire) -> None:
Expand Down
Loading
Loading