feat!: Explicit effects system, prevent calls from annotated funcs to those with fewer effects#1723
feat!: Explicit effects system, prevent calls from annotated funcs to those with fewer effects#1723acl-cqc wants to merge 80 commits into
Conversation
…...should this be in `Context`?
…n Context and check_bb/cfg/etc.
|
This PR contains breaking changes to the public Python API. Breaking changes summary |
|
| Branch | acl/max_effects |
| Testbed | Linux |
Click to view all benchmark results
| Benchmark | hugr_bytes | Benchmark Result bytes x 1e3 (Result Δ%) | Upper Boundary bytes x 1e3 (Limit %) | hugr_nodes | Benchmark Result nodes (Result Δ%) | Upper Boundary nodes (Limit %) |
|---|---|---|---|---|---|---|
| tests/benchmarks/test_big_array.py::test_big_array_compile | 📈 view plot 🚷 view threshold | 158.77 x 1e3(0.00%)Baseline: 158.77 x 1e3 | 160.36 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 6,641.00(0.00%)Baseline: 6,641.00 | 6,707.41 (99.01%) |
| tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_compile | 📈 view plot 🚷 view threshold | 27.53 x 1e3(-0.01%)Baseline: 27.54 x 1e3 | 27.81 x 1e3 (99.00%) | 📈 view plot 🚷 view threshold | 1,074.00(0.00%)Baseline: 1,074.00 | 1,084.74 (99.01%) |
| tests/benchmarks/test_queue_push_pop.py::test_queue_push_benchmark_compile | 📈 view plot 🚷 view threshold | 10.91 x 1e3(0.00%)Baseline: 10.91 x 1e3 | 11.02 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 308.00(0.00%)Baseline: 308.00 | 311.08 (99.01%) |
| tests/benchmarks/test_queue_push_pop.py::test_queue_push_pop_benchmark_compile | 📈 view plot 🚷 view threshold | 14.84 x 1e3(0.00%)Baseline: 14.84 x 1e3 | 14.99 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 435.00(0.00%)Baseline: 435.00 | 439.35 (99.01%) |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1723 +/- ##
==========================================
- Coverage 92.98% 92.91% -0.08%
==========================================
Files 135 138 +3
Lines 13056 13303 +247
==========================================
+ Hits 12140 12360 +220
- Misses 916 943 +27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This reverts commit 41b68c68f112276a24747ccc0e76fd78aa147ae4.
mark-koch
left a comment
There was a problem hiding this comment.
Nice! The implementation looks good to me, just some questions and suggestions for the interface and error messages
| raise GuppyError(LinearComptimeError(node.right, ty)) | ||
| case ast.Call(func=ast.Name(id="effects")) as fx: | ||
| if not isinstance(ty, FunctionType): | ||
| raise GuppyError(InvalidFlagError(node.right)) |
There was a problem hiding this comment.
Could you add tests for each before merging?
| # We might want to support ast.Attribute with LHS "Effects" | ||
| # and look at RHS |
There was a problem hiding this comment.
I think users shouldn't think of effects as an enum. In particular, libraries may declare their own effects so they can come from different namespaces
At the moment we only have ANY so this isn't an issue, but imo not worth spending a lot of time on this until we figure out how new effects should be declared
| arrow = ( | ||
| "->" | ||
| if ty.declared_effects is None | ||
| else f"-{Effect.format_list(ty.declared_effects)}->" |
There was a problem hiding this comment.
Mhmm I think this makes the error message so much worse that we shouldn't release it to users as is.
Maybe for now we could do an inversion of defaults: Regular arrow means ANY effects and have a special thing for empty effects?
| for inp in sig.inputs | ||
| ) | ||
| s += ", ..." if has_var_args else "" | ||
| # TODO Not clear how to display effects in a Python-like syntax? (skip for now) |
There was a problem hiding this comment.
Maybe def foo(x : int) -> int @ effects(...)?
But again, we should only print it if the effects are not the default
| # Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly | ||
| # declare the effects of `f` |
| # When we have effect variables, we should use an existential | ||
| # variable upper-bounded by those allowed in the context. | ||
| exp_sig = exp_sig.with_effects([]) |
There was a problem hiding this comment.
Have you annotated all __bool__ functions in the standard library?
| if ( | ||
| isinstance(d, ast.Call) | ||
| and isinstance(d.func, ast.Name) | ||
| and d.func.id == "guppy" |
There was a problem hiding this comment.
This sort of string matching will break for e.g. guppy-ft where they define their own decorators that internally call guppy
There was a problem hiding this comment.
Don't know what the best solution is, but this seems too fragile. Especially if you're throwing an InternalGuppyError when the decorator is not found
There was a problem hiding this comment.
Ok yes good point about custom decorators. Tricky to see what to do though.
- It seems hard to identify a custom-decorator (that calls guppy) from the AST, there's so little to go on (we haven't even identified the custom-decorator definition/implementation that the
ast.Callpoints to!). - When the decorator executes, it calls
guppy. So the latter could be an opportunity to put something into the e.g.GuppyFunctionDefinition. However at this point it's hard to identify which bit of AST lead to the call to guppy.- Best hack I can see might be for
guppyto see the source file of the decorated function, look up the call stack (for callers to guppy) until it finds one in that source file (?!) and note that somehow. Doesn't work well as if the definition of the custom decorator is in the same source file as it's use...could we look up the call stack (callingguppy) until we find something that's listed as a decorator of the AST node for the function thatguppyis running on? That seems hard, and possibly fragile, but I can have a go...[maybe in another PR?]
- Best hack I can see might be for
- Alternatively we make to make custom decorators do some work themselves (some will have to add an
effects=parameter too but custom decorators might be pure-only or some such)...even this I'm not quite clear on though; we could add an extraeffects_declared_atparameter to@guppyand get them to fill that in, but even finding the AST node to pass to that parameter could be quite hard for them. - Would
@guppy @ effects(...)help? (Would it even work? Perhaps if that was not using__rmatmul__but if@effectswas its own decorator....then we could spot it independently of the@guppy/@custom, although it wouldn't work well if the custom-decorator specified the effects itself)
Least improvement is to fall back to the def line or the whole declaration if we can't find a decorator, rather than InternalError - I'll certainly do that.
There was a problem hiding this comment.
If we can't find the decorator, then the span of the FunctionDef includes the function body but not the decorator list, which isn't much good - so even just this fallback was more awkward than expected. But done. (We can make an issue for providing some way for custom decorators to override that and do better but for another PR?)
| # For now we assume the nested function has the same effects as that enclosing. | ||
| # We could do better by allowing a separate annotation (rather than a parameter | ||
| # to @guppy), but we will wait for callgraph analysis to compute precisely: | ||
| # nested functions are not part of any public API, so changes are not breaking. |
There was a problem hiding this comment.
Does this guarantee that programs that compile with this version will still compile once we do callgraph analysis?
There was a problem hiding this comment.
Not if they do things with Callable, e.g.
@guppy(effects=[ANY])
def outer() -> Callable[[int], int, ANY]:
result(...) # Just to justify the ANY on the outer function, not otherwise relevant
def inner(x: int) -> int:
return x + 1
return inner
callgraph analysis figures out that inner is pure, but Callable is invariant.
However with #1760 and existentials for the effects of the returned Callable then you could make it work if you declared def outer() -> Callable[[int],int]
| from collections.abc import Callable, Sequence | ||
| from types import FrameType | ||
|
|
||
| from guppylang import Effect |
There was a problem hiding this comment.
Yeah this seems wrong. At the very least we should guard behind if TYPE_CHECKING
…ith effects only if hidden
closes #1745.
However, note this is pure typechecking, and does not add any new order edges (ideally we would use the effect-system information to replace
EXTENSION_OPS_WITH_SIDE_EFFECTSbut leaving that to the PR that solves #1746.ANY.[Effect.ANY]@effectsmodifier; without which, you get[ANY]by default.Do I want more tests of the quantum stdlib? Any ideas what?
BREAKING CHANGE: Functions taking Callable will be able to accept either only Callables with effects (the default) or (if annotated) those without, rather than both; those returning Callables will need an explicit annotation if the returnee is pure.