-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Parse inout annotations in function signatures #316
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/inout #316 +/- ##
==============================================
+ Coverage 92.62% 92.63% +0.01%
==============================================
Files 45 45
Lines 5165 5188 +23
==============================================
+ Hits 4784 4806 +22
- Misses 381 382 +1 ☔ View full report in Codecov by Sentry. |
Guppy compilation failed. Error in file $FILE:14 | ||
|
||
12: @guppy.declare(module) | ||
13: def foo(f: Callable[[], qubit @inout]) -> None: ... | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
GuppyError: `@` type annotations are not allowed in this position |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The broad error location is not ideal, but improving this requires a bigger refactor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so I've written some quite long comments suggesting a couple of big refactors. I think commoning up check_signature
with _CallableTypeDef.check_instantiate
should be done, unless it's much harder than it looks, and (if you do nothing else) then I'd do FlaggedArgs
-> FlaggedArg
.
But I'd also think seriously about breaking up arg_from_ast
, and switching from tuple[type, flags]
to a broader union (TypeArg | ConstArg | InoutTypeArg
, say - maybe there are benefits in the approach taken by python's Annotated
), even if you don't go that way.
guppylang/tys/parsing.py
Outdated
|
||
|
||
def arg_from_ast( | ||
node: AstNode, | ||
globals: Globals, | ||
param_var_mapping: dict[str, Parameter] | None = None, | ||
) -> Argument: | ||
) -> tuple[Argument, InputFlags]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arg! (argh!) Sorry but I got getting really confused here; what is an Argument
? Is it a value Hugr static TypeArg? (It certainly looks like one) - but then we wouldn't want it to be inout
or have InputFlags, would we? More comments would really help, maybe also think about renaming Argument->StaticArg, say.
I think arg_from_ast
parses both TypeArgs (Hugr TypeArgs of kind TypeParam::Type), and ConstArgs (Hugr TypeArgs of other kinds). But, hang on, ConstArgs are not function parameters (that'd be like def foo(x: 6)
!). And @inout
is only appropriate for the top/outermost level argument to a def
, or to Callable
, yet we make life difficult for all the other check_instantiate
s in order to fit in Callable
(e.g. list[int @inout]
) - I see 5 calls to check_no_flags, plus a couple that have no args to have any flags.
- I think passing the flag by subclassing could be an easier route, you might still have an equivalent of
check_no_flags
but I reckon it might just fall out of normalcheck_all_args
or checking-against-TypeParam (that is: make a normal Hugr TypeParam::Type unable to accept an InoutTypeArg, but Callable has a special TypeParam::MaybeInOutType that can accept both InoutTypeArg and normal TypeArg). - Alternatively....this'll be even more controversial....can you special-case Callable?
Generally I think I'd favour breaking the lengthy arg_from_ast
up into flagged_arg_from_ast
or (maybe_
)inout_arg_from_ast
that looks for BinOp with @
and then (usually) falls back to a shorter arg_from_ast
that does not support that case, and thus, returns a TypeArg (| ConstArg
) only. (Or returns Argument without separate flags, that'd be a first/lesser step from what's in the PR now).
Which is to say that (my gut feeling is) you'd do better to push error checking downwards towards the parsing/leaves, rather than parsing whatever and then raising error messages (like, found function parameter of type 6). Not sure if that would help with the error locations. Then, say, maybe_inout_arg_from_ast
would (if not BinOp) call type_arg_from_ast
(that returns TypeArg, and does not support ConstArg), and then there is const_or_type_arg_from_ast
(used for arguments to parameterized types), that kind of thing.
Note you've already gone this route with type_from_arg
vs type_with_flags_from_arg
, so it's not thaaaat radical....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course we should also think about likely future annotations. list[MyStruct @frozen]
is plausible, true....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arg! (argh!) Sorry but I got getting really confused here; what is an Argument? Is it a value Hugr static TypeArg?
Sorry for the confusion, yes Argument
is Guppy's version of static arguments to types. E.g. the type array[int, 5]
has a TypeArg
int
and a ConstArg
5
.
More comments would really help, maybe also think about renaming Argument->StaticArg, say.
I added #358 to consider the rename
And
@inout
is only appropriate for the top/outermost level argument to a def, or to Callable, yet we make life difficult for all the other check_instantiates in order to fit in Callable (e.g.list[int @inout]
) [...] Alternatively....this'll be even more controversial....can you special-case Callable?
After reflecting a bit on this I think that special casing Callable
is probaby the best solution in the near term. I initially wanted a general solution that can handle future flags like @frozen
etc., but I agree it adds a lot of complexity and probably isn't worth it for now. I like your subclassing idea though, I'll probably go for that if we want more general flags in the future 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The neatest solution here (I think) is adding a new TypeParam. However it's definitely worth having a think about what the future flags might be - Frozen<T>
might be a lot like T
(i.e. acceptable anywhere a T is?), so probably needs special consideration, whereas inout
is clearly a bit special (it's not really a property of the parameter but of the whole function signature).
Special-casing Callable might be fine in the short-term, tho, and maybe even in the longer term if it turns out we don't need similar specials for any of the others...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Special-casing Callable might be fine in the short-term, tho, and maybe even in the longer term if it turns out we don't need similar specials for any of the others...
Agreed, I think I'll do that for now 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another benefit of special casing Callable
is that we can now properly handle the list syntax Callable[[Arg1, Arg2], Return]
without the flattening hack
guppylang/definition/ty.py
Outdated
|
||
if TYPE_CHECKING: | ||
from guppylang.checker.core import Globals | ||
|
||
|
||
FlaggedArgs: TypeAlias = Sequence[tuple[Argument, InputFlags]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most uses have gone from Sequence[Argument]
to FlaggedArgs
, but you do have a couple of tuple[Argument, InputFlags]
elsewhere. Maybe define FlaggedArg = tuple[...]
and use that (so Sequence[FlaggedArg]
in many places)....albeit, elsewhere I suggest a bigger redifinition of FlaggedArg ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This no longer applied after ff545a7
@@ -55,6 +55,16 @@ def py(*_args: Any) -> Any: | |||
raise GuppyError("`py` can only by used in a Guppy context") | |||
|
|||
|
|||
class _Inout: | |||
"""Dummy class to support `@inout` annotations.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to allow def foo(x: int @inout)
, since Python (in its infinite wisdom cough splutter boo) disallows def foo(x: @inout int)
. Obviously, it's fine to do matrix multiplication inside a type annotation but not invoke a decorator....
However, have you thought about the "pythonic" alternative, which I believe would be def foo(x: Annotated[int, "inout"]
?@inout
is a bastardization of python syntax anyway so what would users think? I admit Annotated
is rather long - what about def foo(x: Inout[int])
? (No way, I hear you say - and I see the downside - maybe that lengthy Annotated isn't sooo bad, then....)
Not seriously recommending you change, but how different / how much simpler might that make things, would it impact the design at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created #359 for discussion.
Not seriously recommending you change, but how different / how much simpler might that make things, would it impact the design at all?
I think this is mostly a syntax concern. The design wouldn't be affected much imo
guppylang/checker/func_checker.py
Outdated
@@ -148,8 +148,13 @@ def check_signature(func_def: ast.FunctionDef, globals: Globals) -> FunctionType | |||
for inp in func_def.args.args: | |||
if inp.annotation is None: | |||
raise GuppyError("Argument type must be annotated", inp) | |||
ty = type_from_ast(inp.annotation, globals, param_var_mapping) | |||
inputs.append(FuncInput(ty, InputFlags.NoFlags)) | |||
ty, flags = type_with_flags_from_ast(inp.annotation, globals, param_var_mapping) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does seem a bit like, we have two places that define the set(s) of flags legal in the signature of a function - _CallableTypeDef
also makes similar checks. If you parsed type_with_flags_from_ast
for the return type too, you could call some single checking function (maybe it takes a list[FlaggedArg]
and a single FlaggedArg
and then _CallableTypeDef
first separates out the last element of the list) that issues appropriate errors and returns some appropriate e.g. (list[TypeArg],TypeArg)
guppylang/tys/parsing.py
Outdated
param_var_mapping: dict[str, Parameter] | None = None, | ||
) -> tuple[Type, InputFlags]: | ||
"""Turns an AST expression into a Guppy type possibly annotated with @flags.""" | ||
# Parse an argument and check that it's valid for a `TypeParam` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's amusing to think that we could have got this far with x: 6 @inout
;-). But I'm reassured that you are making the right check!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for changes @mark-koch, looks good to me! :). Improvement in error location is nice too.
I wonder about also being able to parse Annotated[<type>, inout]
just so that there is an allowed "pythonic" syntax too (we claim guppy is pythonic after all! And then we can see what people do with it), but I suspect that's more effort than it's worth...
"`@` type annotations are not allowed in this position", loc | ||
) | ||
return FunctionType([FuncInput(ty, flags) for ty, flags in inputs], output_ty) | ||
# Callable types are constructed using special login in the type parser |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Callable types are constructed using special login in the type parser | |
# Callable types are constructed using special logic in the type parser |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could consider moving some of that code into CallableTypeDef itself, just so you could have a comment "Callable types are constructed using instantiate_func_type()" or something like that, but this is fine
return arg_from_ast(stmt.value, globals, param_var_mapping) | ||
except (SyntaxError, ValueError): | ||
raise GuppyError("Invalid Guppy type", node) from None | ||
node = _parse_delayed_annotation(node.value, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, breaking that out is a good move :)
globals: Globals, | ||
param_var_mapping: dict[str, Parameter] | None, | ||
) -> FunctionType: | ||
"""Helper function to parse a `Callable[[<arguments>], <return types>]` type.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Helper function to parse a `Callable[[<arguments>], <return types>]` type.""" | |
"""Helper function to parse a `Callable[[<arguments>], <return type>]` type.""" |
Or are multiple return types allowed??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it should only be a single return type (that could be a tuple of multiple types) 👍
"""Helper function to parse a `Callable[[<arguments>], <return types>]` type.""" | ||
err = ( | ||
"Function types should be specified via " | ||
"`Callable[[<arguments>], <return types>]`" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
"`Callable[[<arguments>], <return types>]`" | |
"`Callable[[<arguments>], <return type>]`" |
I wonder about also noting, this is standard python hyping!
"""Turns an AST expression into a Guppy type with some optional @flags.""" | ||
# Check for `type @flag` annotations | ||
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult): | ||
ty, flags = type_with_flags_from_ast(node.left, globals, param_var_mapping) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note this means x @inout @inout
is also allowed and has the same effect as only once
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I think that should be fine though
This is the dev branch for the inout argument feature (tracked by #282). The idea is to allow explicit `@inout` annotations on function arguments that "give back" the passed value after the function returns: ```python @guppy def foo(q: qubit @inout) -> None: ... @guppy def bar(q1: qubit @inout, q2: qubit @inout) -> bool: ... @guppy def main() -> None: q1, q2 = qubit(), qubit() foo(q1) # Desugars to `q1 = foo(q1)` x = bar(q1, q2) # Desugars to `q1, q2, x = bar(q1, q2)` y = bar(q1, q1) # Error: Linearity violation, q1 used twice ``` To enable this, we need to enforce that `@inout` arguments are not moved in the body of the function (apart from passing them in another `@inout` position). This means that the argument will always be bound to the same name and never aliased which allows us to desugar `@inout` functions like ```python @guppy def bar(q1: qubit, q2: qubit) -> bool: [body] return [expr] ``` into ```python @guppy def bar(q1: qubit, q2: qubit) -> tuple[qubit, qubit, bool]: [body] return q1, q2, [expr] # Linearity checker needs to ensure that q1, q2 are unused ``` Note that we only allow `@inout` annotations on linear types, since they would be useless for classical ones (unless we also implement an ownership system for classical values). Supporting them would make the checking logic more complicated without providing any meaningful benefit. Tracked PRs: * #315 * #316 * #349 * #344 * #321 * #331 * #350 * #339 * #340 * #351
🤖 I have created a release *beep* *boop* --- ## [0.10.0](v0.9.0...v0.10.0) (2024-09-11) ### ⚠ BREAKING CHANGES * Bumped the `hugr` dependency to `0.8.0` * `GuppyModule.load` no longer loads the content of modules but instead just brings the name of the module into scope. Use `GuppyModule.load_all` to get the old behaviour. * Removed `guppylang.hugr_builder.hugr.Hugr`, compiling a module returns a `hugr.Package` instead. ### Features * Add `__version__` field to guppylang ([#473](#473)) ([b996c62](b996c62)) * Add angle type ([#449](#449)) ([12e41e0](12e41e0)) * Add array literals ([#446](#446)) ([a255c02](a255c02)) * Add equality test for booleans ([#394](#394)) ([dd702ce](dd702ce)), closes [#363](#363) * Add pi constant ([#451](#451)) ([9d35a78](9d35a78)) * Add qualified imports and make them the default ([#443](#443)) ([553ec51](553ec51)) * Allow calling of methods ([#440](#440)) ([5a59da3](5a59da3)) * Allow imports of function definitions and aliased imports ([#432](#432)) ([e23b666](e23b666)) * Array indexing ([#415](#415)) ([2199b48](2199b48)), closes [#421](#421) [#422](#422) [#447](#447) * Inout arguments ([#311](#311)) ([060649b](060649b)), closes [#315](#315) [#316](#316) [#349](#349) [#344](#344) [#321](#321) [#331](#331) [#350](#350) [#340](#340) [#351](#351) * range() with single-argument ([#452](#452)) ([d05f369](d05f369)) * Skip checking of redefined functions ([#457](#457)) ([7f9ad32](7f9ad32)) * Support `nat`/`int` ↔ `bool` cast operations ([#459](#459)) ([3b778c3](3b778c3)) * Use `hugr-cli` for validation ([#455](#455)) ([1d0667b](1d0667b)) * Use cell name instead of file for notebook errors ([#382](#382)) ([d542601](d542601)) * Use the hugr builder ([536abf9](536abf9)) ### Bug Fixes * Fix and update demo notebook ([#376](#376)) ([23b2a15](23b2a15)) * Fix linearity checking bug ([#441](#441)) ([0b8ea21](0b8ea21)) * Fix struct definitions in notebooks ([#374](#374)) ([b009465](b009465)) ### Documentation * Update readme, `cargo build` instead of `--extra validation` ([#471](#471)) ([c2a4c86](c2a4c86)) ### Miscellaneous Chores * Update hugr to `0.8.0` ([#454](#454)) ([b02e0d0](b02e0d0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Closes #309. See #282 for context.