Skip to content

feat!: Explicit effects system, prevent calls from annotated funcs to those with fewer effects#1723

Open
acl-cqc wants to merge 80 commits into
mainfrom
acl/max_effects
Open

feat!: Explicit effects system, prevent calls from annotated funcs to those with fewer effects#1723
acl-cqc wants to merge 80 commits into
mainfrom
acl/max_effects

Conversation

@acl-cqc
Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc commented May 7, 2026

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_EFFECTS but leaving that to the PR that solves #1746.

  • The annotation takes a list of Effect; I've added both internal and frontend enums but these might be combined. At present the only known effect is ANY.
  • Unannotated functions are assumed to have [Effect.ANY]
  • Callable types can be given effects via an @effects modifier; without which, you get [ANY] by default.
    • Callables are invariant. We could perhaps be more flexible, while we are using Order edges not tokens, but that would be introducing subtyping; moreover this is consistent, and will be improved in Shorthand syntax for Effect Variables #1760 (as this does cause some degradations).
  • Nested functions are assumed to have the same effects as their containing function (for now; this could cause issues if you want to return one as a Callable return value)
  • Changes made to stdlib, and compiler changes to support them by making conversion-to-bool, enum and struct constructors all "pure", are all gathered together in 4523d81 (you can see without the PR without these: https://github.com/Quantinuum/guppylang/pull/1723/changes/BASE..51745bfdb8c55a24c57d783a0a5d1b91c3613781

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.

@hugrbot
Copy link
Copy Markdown
Collaborator

hugrbot commented May 7, 2026

This PR contains breaking changes to the public Python API.

Breaking changes summary
guppylang-internals/src/guppylang_internals/definition/overloaded.py:0: OverloadNoMatchError.__init__(max_effects_from):
Parameter was added as required

guppylang-internals/src/guppylang_internals/checker/cfg_checker.py:73: check_cfg(first_modifier_node):
Positional parameter was moved
Details: position: from 6 to 7 (+1)

guppylang-internals/src/guppylang_internals/checker/cfg_checker.py:73: check_cfg(max_effects_from):
Parameter was added as required

guppylang-internals/src/guppylang_internals/checker/cfg_checker.py:228: check_bb(max_effects_from):
Parameter was added as required

guppylang-internals/src/guppylang_internals/checker/modifier_checker.py:22: check_modified_block(max_effects_from):
Parameter was added as required


@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🐰 Bencher Report

Branchacl/max_effects
TestbedLinux
Click to view all benchmark results
Benchmarkhugr_bytesBenchmark Result
bytes x 1e3
(Result Δ%)
Upper Boundary
bytes x 1e3
(Limit %)
hugr_nodesBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 7, 2026

Codecov Report

❌ Patch coverage is 97.70992% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.91%. Comparing base (82b9c77) to head (74d1453).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
guppylang/src/guppylang/decorator.py 88.23% 2 Missing ⚠️
guppylang/src/guppylang/std/effects.py 77.77% 2 Missing ⚠️
...ls/src/guppylang_internals/checker/func_checker.py 96.15% 1 Missing ⚠️
...uppylang-internals/src/guppylang_internals/span.py 85.71% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 7, 2026

Merging this PR will not alter performance

✅ 9 untouched benchmarks


Comparing acl/max_effects (74d1453) with main (db54090)

Open in CodSpeed

@acl-cqc acl-cqc marked this pull request as ready for review May 26, 2026 19:07
@acl-cqc acl-cqc requested a review from a team as a code owner May 26, 2026 19:07
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch left a comment

Choose a reason for hiding this comment

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

Nice! The implementation looks good to me, just some questions and suggestions for the interface and error messages

Comment thread guppylang-internals/src/guppylang_internals/tracing/function.py Outdated
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))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you add tests for each before merging?

Comment on lines +481 to +482
# We might want to support ast.Attribute with LHS "Effects"
# and look at RHS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)}->"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe def foo(x : int) -> int @ effects(...)?

But again, we should only print it if the effects are not the default

Comment thread tests/error/poly_errors/non_linear2.py Outdated
Comment on lines +10 to +11
# Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly
# declare the effects of `f`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't see the connection to #1752. Isn't this just because of invariance?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I meant #1760, which follows on from that

Comment on lines +1520 to +1522
# 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([])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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"
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch Jun 2, 2026

Choose a reason for hiding this comment

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

This sort of string matching will break for e.g. guppy-ft where they define their own decorators that internally call guppy

Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch Jun 2, 2026

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.Call points 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 guppy to 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 (calling guppy) until we find something that's listed as a decorator of the AST node for the function that guppy is running on? That seems hard, and possibly fragile, but I can have a go...[maybe in another PR?]
  • 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 extra effects_declared_at parameter to @guppy and 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 @effects was 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?)

Comment on lines +203 to +206
# 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this guarantee that programs that compile with this version will still compile once we do callgraph analysis?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah this seems wrong. At the very least we should guard behind if TYPE_CHECKING

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kwarg to @guppy for ordering requirements

4 participants