Replies: 6 comments 7 replies
-
|
This sounds like a great idea! q = H(X(qubit()))by defining X and H using @guppy
def X(q: qubit) -> qubit @borrows(q): ...
@guppy
def H(q: qubit) -> qubit @borrows(q): ...with the trivial @guppy.unborrow(X)
def _X_unborrow(q: qubit @owned) -> qubit:
return q
@guppy.unborrow(H)
def _H_unborrow(q: qubit @owned) -> qubit:
return qCan we also consider adding built-in higher-order function q = with_owned(h)(with_owned(x)(qubit()))without user-defined |
Beta Was this translation helpful? Give feedback.
-
|
I really like the idea in general, though I think having to provide a function to unborrow (maybe |
Beta Was this translation helpful? Give feedback.
-
|
Great idea. Question: it is not obvious to me what the type of the unborrows function is supposed to be? In the case of |
Beta Was this translation helpful? Give feedback.
-
|
I like this idea, and I think there’s even more we could explore here. Consider the example: @guppy.struct
class MyStruct:
q: qubit
x: int
@guppy
def foo(q: qubit) -> MyStruct @borrows(q):
h(q)
# We're allowed to treat q as owned, since it's listed in @borrows
r = q
return MyStruct(r, 42)
@guppy
def bar(s: MyStruct) -> None: ...
@guppy
def baz(q: qubit) -> None:
bar(foo(q)) # Ok!Desugared, this expands roughly to: _s = foo(q)
_s = bar(_s)
q = _foo_unborrow(_s)The inline notation here is essentially encoding Rust-like lifetime information. So why not allow this directly, without requiring annotations? Rewriting the previous example, the intent becomes clearer: @guppy
def baz(q: qubit) -> None:
# q is available
...
s = foo(q) # s is now live
... # q is not allowed here
bar(s) # last use of s
# q becomes usable again hereThis mirrors Rust’s lifetime model: the lifetime of We might even express this explicitly with a @guppy
def baz(q: qubit) -> None:
# q is available
...
with s = foo(q): # s is live within this block
...
bar(s) # last use of s
# q is available again here
measure(q) |
Beta Was this translation helpful? Give feedback.
-
|
This would be nice to avoid some of the struct unpacking code I am having to write at the moment. Would it desirable to add some syntax to give a clue that variables are borrowed? E.g. in an above example, it is not necessarily clear from the callsite that this is happening. Maybe there is no pythonic way to do this other than |
Beta Was this translation helpful? Give feedback.
-
|
Would this system allow something like this? (Sorry if I got guppy syntax wrong) @guppy.struct
class MyStruct:
b: bool
@guppy
def foo(q: qubit) -> MyStruct @borrows(q):
h(q)
# We're allowed to treat q like an owned value since it's mentioned
# in the `@borrows` annotation, so presumably we can measure it
b = measure q
return MyStruct(b)
@guppy.unborrow(foo)
def _foo_unborrow(v: MyStruct @owned) -> qubit:
# I don't know the syntax but allocate a new qubit here
return new_qubit |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
The Problem
Currently, Guppy only allows borrowing of values for the scope of a function call. In particular, we don't allow returning borrowed values or storing them in data structures.
We made this restriction to avoid Rust-style lifetime annotations, however it is quite annoying sometimes:
Calling
foois impossible since we're not allowed to pack the borrowed qubits into an array.Similarly, we run into this issue when trying to implement the "iterators without ownership" language feature:
Constructing the
zipiterator would require borrowingqs1andqs2for the duration of the entire loop which is not possible using the current borrowing semantics.Luckily, I think I came up with a way to solve these problems without requiring lifetime annotations!
Proposal
Basic Example
We add a new
@borrowsannotation for function return types that specifies that the returned value contains some borrowed inputs:Whenever the user writes a function with an
@borrowsreturn annotation, we also force them to register a second function that "unborrows" the value (better name suggestions welcome :D).Calls to
foowill only be allowed in borrowed positions:The compiler desugars the
bar(foo(q))call intoNeed for unborrow function
Note that the unborrow function needs to be user-provided and cannot be inferred by the compiler in general:
Here, the unborrow logic depends on runtime information:
Of course, the user could provide an unborrow implementation that is incorrect, in which case they would get unexpected semantics. We shouldn't try to validate the user implementation and just take it at face value. If it's wrong then it's the user's fault!
Performance
Note that the computation inside unborrow functions should ideally be cheap. Indeed, all cases I'm thinking of that are needed for the standard library just involve unpacking a struct or index into an array.
Of course the system could also be abused and imo it wouldn't be worth trying to prevent that - if the user decides to write an unborrow function with bad time complexity, then they're responsible for the performance hit.
Standard Library
In general, I'm envisioning this as an advanced feature that is mostly only used in the standard library. E.g. having borrowing versions of array and tuple constructors as well as iterator combinators would already go a long way in improving usability, without the users having to write unborrow functions themselves.
Importantly, these alternative array constructor should borrow all passed inputs - we can't handle arrays containing a mix of borrowed and owned values.
Structs
We could also consider similar annotations on struct types:
In that case, the auto-generated constructor would have the following signature:
Non-borrowed Inputs
As seen in the previous examples, we should also allow
@borrowsfunctions to take other inputs. However, there is the following question:Should we allow other non-copyable + owned inputs?
If we allow this, then they must be removed from the signature of the unborrow function - otherwise we wouldn't be able to emit the unborrow call since the previous value has been consumed!
Connection to Subscripts
Note that this proposal basically generalises the current system we have for borrowing out of subscripts. When borrowing
x[idx], we callx.__getitem__(idx)and make sure that the "unborrow function"x.__setitem__is called when the borrow expires.Implementation-wise, this means that this feature probably wouldn't be that much work, we just need to generalise the
SubscriptAccessplace to work for arbitrary pairs borrow-unborrow functions.Connection to Aeneas
This proposal is actually equivalent to what Aeneas is doing: They show how lifetime annotations in Rust code can be used to automatically generate these unborrow functions. We're doing it the other way around: We make people write unborrow functions so they don't have to deal with lifetime annotations.
Assigning Borrowed Values
An optional orthogonal extension would be to allow assigning borrowed values. This is motivated by the fact that following would still fail to compile following the proposal up to this point:
Instead, we would have to call
foo(array(q1, q2)). Allowing assignments of borrowed values to temporary variables would make things more convenient.However, this would require some significant changes to the borrow checker. Some relevant literature from Rust:
I think this would be a good idea longer term, but I need to think more about it before making a concrete proposal.
Beta Was this translation helpful? Give feedback.
All reactions