Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
66a397e
Decorator add max_effects as list[str]
acl-cqc May 4, 2026
d5815fc
Make annotation be list[Effect] with empty Enum
acl-cqc May 4, 2026
b28cdbd
Add field to Raw(Traced)FunctionDef,Decl, store string values in Pars…
acl-cqc May 4, 2026
96b9346
Add field and copy through into Parsed/Checked/Compile variants
acl-cqc May 4, 2026
9a71a83
Add max_effects parameter to many funcs, with default for StmtChecker…
acl-cqc May 6, 2026
d8632ba
Remove from StmtChecker, add to Context
acl-cqc May 6, 2026
2afa232
driveby common-up FunctionType constructions in decorator.py
acl-cqc May 6, 2026
4d256e5
Remove from Parsed def/decl (only Raw), add to FunctionType - still i…
acl-cqc May 6, 2026
706f062
check_nested_func_def gets max_effects from Context
acl-cqc May 7, 2026
1067f5c
Add TooManyEffectsError, check+raise in toplevel check_call+synthesiz…
acl-cqc May 6, 2026
10b682c
First success/failure tests; no binary ops yet, and error messages ne…
acl-cqc May 7, 2026
6f92edb
Add test of calling result...aaiieeeee
acl-cqc May 7, 2026
595e0bb
Add max_effects to @hugr_op and RawCustomFunctionDef
acl-cqc May 7, 2026
c1b6439
Use int+ in tests, breaking
acl-cqc May 7, 2026
dd1245d
Declare int+ as having no effects...fixes tests
acl-cqc May 7, 2026
28454b4
test cleanups (don't import array), inc driveby bracket-removal
acl-cqc May 11, 2026
be231e0
Add error test: Callable has unknown effects
acl-cqc May 11, 2026
6ee59a3
refactor: Common up effects check
acl-cqc May 11, 2026
dbc5fcb
improve error location when possible
acl-cqc May 11, 2026
87959e6
Parse Callable[[inp,...],out,max_effects]
acl-cqc May 11, 2026
8782e3e
pure_calls_impure_def: remove comment about result, we've done that now
acl-cqc May 11, 2026
6224053
test no contravariance of argument type
acl-cqc May 11, 2026
4284ee9
no, test no covariance of return type, simpler right
acl-cqc May 11, 2026
8965c2e
Enforce invariance of Callable max-effects when assignment, +test +pr…
acl-cqc May 11, 2026
5cf12c7
pretty-print as -[]->
acl-cqc May 11, 2026
ba705c8
list[str](|None) -> list[tys.Effect](|None)
acl-cqc May 11, 2026
797c20b
Show where caller's effects are declared (not callee)
acl-cqc May 17, 2026
ab10061
move comment
acl-cqc May 17, 2026
39c6c8c
Further refine error message, use wrap
acl-cqc May 18, 2026
a307f02
overloading: check effects last, pass on error via isinstance
acl-cqc May 18, 2026
028b8d5
overloaded.py: whitelist errors to ignore rather than suppressing all
acl-cqc May 20, 2026
4e97214
Add local/specific suppress_overload_match_errors contextmanager with…
acl-cqc May 20, 2026
c6c8d47
allow legality of effects to determine overloading, TODO error hint
acl-cqc May 22, 2026
93535dd
Drop the 'with any effects' in overloaded
acl-cqc May 22, 2026
5a8053e
Add internal Effect.ANY; improve formatting; FunctionType.max_effects…
acl-cqc May 22, 2026
622cfbf
Always include allowed-effects in Context even when no decl; worsens …
acl-cqc May 22, 2026
078c2c8
Revert "Always include allowed-effects in Context even when no decl; …
acl-cqc May 22, 2026
2aae8da
Allow ANY in frontend, inc for Callables; test [ANY] vs no annotation
acl-cqc May 22, 2026
5c761ed
Re-export guppylang.Effect, switch hugr_op/custom_func decorators
acl-cqc May 22, 2026
af1b7f2
num.py: label funcs as max_effects=[], except some panicking, and boo…
acl-cqc May 22, 2026
0173ac0
conversion to bool with no effects - sadly a hard requirement for 'tr…
acl-cqc May 22, 2026
645b10c
Mark many quantum funcs are pure, also angle. But, breaks because ang…
acl-cqc May 26, 2026
339389d
Add tests of higher-order effects (part-xfailed)
acl-cqc May 26, 2026
cba65d5
Mark default Struct constructor as no-effects, update error messages
acl-cqc May 26, 2026
b1488a3
Merge remote-tracking branch 'origin/main' into acl/max_effects
acl-cqc May 26, 2026
9fabd5c
max_effects => effects
acl-cqc May 26, 2026
442033e
max_effects_declared -> declared_effects
acl-cqc May 26, 2026
5f22a66
doc comment max_effects_from / declared_effects
acl-cqc May 26, 2026
279a4ab
typing_extensions.assert_never
acl-cqc May 26, 2026
8b662c5
Error messages shortened by rename (!)
acl-cqc May 26, 2026
06533b2
fix test_struct load_constructor
acl-cqc May 26, 2026
dcfe743
Add collections.abc import for 3.10
acl-cqc May 26, 2026
8cbd11a
...and update error messages
acl-cqc May 26, 2026
6163444
make enum-constructors also effect-free
acl-cqc May 26, 2026
328f8de
WIP @ effects. Referring to Effects.ANY is a PITA
acl-cqc May 26, 2026
7e80ed8
move @effects to std/effects.py
acl-cqc May 26, 2026
6976d3b
comments
acl-cqc May 26, 2026
4689bd4
missed from move of @effects
acl-cqc May 26, 2026
af1f2e5
missing import in fun_ty_mismatch_4
acl-cqc May 26, 2026
51745bf
undo stdlib changes, enum+struct constructors, bool conversion
acl-cqc May 26, 2026
4523d81
Redo stdlib changes, enum+struct constructors, bool conversion
acl-cqc May 26, 2026
78f12ed
Add test w/custom decorator -> InternalError, oops
acl-cqc Jun 5, 2026
db75029
refactor: max_effects_from stores Span
acl-cqc Jun 5, 2026
7b26c25
Give whole FunctionDef as context if can't find decorator...includes …
acl-cqc Jun 5, 2026
63a5b4e
Include decorators, exclude body
acl-cqc Jun 5, 2026
8659f75
Better errors when parsing @ effects
acl-cqc Jun 5, 2026
31b3a69
make decorator more flexible
acl-cqc Jun 5, 2026
e532548
correct issue link
acl-cqc Jun 5, 2026
458de9a
use set when checking invariance
acl-cqc Jun 5, 2026
dac0e57
Skip the explicit list of allowed effects when its in the decorator
acl-cqc Jun 5, 2026
33948ec
change tuple[list[Effect], Span | expr] -> EffectLimitDecl, add s
acl-cqc Jun 5, 2026
fa3ab4e
Add decl_name to EffectLimitDecl and MaxFromDecl
acl-cqc Jun 5, 2026
1f5ff09
TooManyEffectsError: display target name with effects, or just type w…
acl-cqc Jun 5, 2026
74d1453
drop pipeline comment
acl-cqc Jun 5, 2026
3810760
Remove external decorator enum Effect, just re-export
acl-cqc Jun 7, 2026
975bb02
Internals decorator use internals.tys.Effect not re-export
acl-cqc Jun 7, 2026
4e64ea8
Tests of comptime (some failing)
acl-cqc Jun 6, 2026
d5b45af
Add comptime error tests (messages wrong atm)
acl-cqc Jun 6, 2026
281501f
Refactor EffectLimitDecl.for_def
acl-cqc Jun 8, 2026
d7665a2
Implement for tracing, update error messages
acl-cqc Jun 8, 2026
8ef3a39
Add backquotes to some error messages
acl-cqc Jun 11, 2026
1ffbeb1
undo purification of enum+struct ctors, bool conversion, stdlib excep…
acl-cqc May 26, 2026
01d26e4
Merge remote-tracking branch 'origin/effects' into acl/max_effects
acl-cqc Jun 11, 2026
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 @@ -15,6 +15,7 @@
from guppylang_internals.cfg.cfg import CFG, BaseCFG
from guppylang_internals.checker.core import (
Context,
EffectLimitDecl,
Globals,
Locals,
Place,
Expand Down Expand Up @@ -76,6 +77,7 @@ def check_cfg(
generic_args: dict[str, Argument],
func_name: str,
globals: Globals,
max_effects_from: EffectLimitDecl | None,
first_modifier_node: ast.expr | None = None,
) -> CheckedCFG[Place]:
"""Instantiates a control-flow graph with the given `generic_args` and then type
Expand All @@ -100,7 +102,13 @@ def check_cfg(
# We start by compiling the entry BB
checked_cfg: CheckedCFG[Variable] = CheckedCFG([v.ty for v in inputs], return_ty)
checked_cfg.entry_bb = check_bb(
cfg.entry_bb, checked_cfg, inputs, return_ty, generic_args, globals
cfg.entry_bb,
checked_cfg,
inputs,
return_ty,
generic_args,
globals,
max_effects_from=max_effects_from,
)
compiled = {cfg.entry_bb: checked_cfg.entry_bb}

Expand All @@ -127,7 +135,13 @@ def check_cfg(
else:
# Otherwise, check the BB and enqueue its successors
checked_bb = check_bb(
bb, checked_cfg, input_row, return_ty, generic_args, globals
bb,
checked_cfg,
input_row,
return_ty,
generic_args,
globals,
max_effects_from=max_effects_from,
)
queue += [
# We enumerate the successor starting from the back, so we start with
Expand Down Expand Up @@ -237,6 +251,7 @@ def check_bb(
return_ty: Type,
generic_args: dict[str, Argument],
globals: Globals,
max_effects_from: EffectLimitDecl | None,
) -> CheckedBB[Variable]:
cfg = bb.containing_cfg

Expand All @@ -261,7 +276,9 @@ def check_bb(
raise GuppyError(_assigned_in_modifier_error(x, use, assignment))

# Check the basic block
ctx = Context(globals, Locals({v.name: v for v in inputs}), generic_args)
ctx = Context(
globals, Locals({v.name: v for v in inputs}), generic_args, max_effects_from
)
checked_stmts = StmtChecker(ctx, bb, return_ty).check_stmts(bb.statements)

# If we branch, we also have to check the branch predicate
Expand Down
59 changes: 58 additions & 1 deletion guppylang-internals/src/guppylang_internals/checker/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import itertools
from collections.abc import Iterable, Iterator
from dataclasses import dataclass, field, replace
from functools import cache, cached_property
from functools import cache, cached_property, reduce
from types import FrameType
from typing import (
TYPE_CHECKING,
Expand All @@ -26,9 +26,12 @@
)
from guppylang_internals.engine import BUILTIN_DEFS, DEF_STORE, ENGINE
from guppylang_internals.error import InternalGuppyError, RequiresMonomorphizationError
from guppylang_internals.span import Span, to_span
from guppylang_internals.tys import Effect
from guppylang_internals.tys.arg import Argument, ConstArg, TypeArg
from guppylang_internals.tys.const import BoundConstVar, ConstValue, ExistentialConstVar
from guppylang_internals.tys.ty import (
FunctionType,
InputFlags,
StructType,
Type,
Expand Down Expand Up @@ -441,13 +444,67 @@ def items(self) -> Iterable[tuple[VId, V]]:
return itertools.chain(self.vars.items(), parent_items)


@dataclass(frozen=True)
class EffectLimitDecl:
"""Records a declaration limiting the effects that may be used in a Context"""

effects: list[Effect]
decl: ast.expr | Span
decl_name: str

@classmethod
def for_def(
cls, ty: FunctionType, func_def: ast.FunctionDef
) -> "EffectLimitDecl | None":
if ty.declared_effects is None:
return None
if (deco := _find_guppy_decorator(func_def.decorator_list)) is not None:
decl = deco
else:
# Could not identify decorator, so include all in context; union with
# returns will include name etc. inbetween but avoid the function body.
elems = func_def.decorator_list
if func_def.returns is not None:
elems += [func_def.returns]

def union(s1: Span, s2: Span) -> Span:
r = s1 | s2
assert r is not None # Function def should not cross file boundary
return r

decl = reduce(union, (to_span(e) for e in elems))

return EffectLimitDecl(
ty.declared_effects,
decl,
func_def.name,
)


def _find_guppy_decorator(decorators: list[ast.expr]) -> ast.expr | None:
for d in decorators:
if (
isinstance(d, ast.Call)
and isinstance(d.func, ast.Name)
and d.func.id == "guppy"
):
return d
if isinstance(d, ast.Name) and d.id == "guppy":
return d
return None


class Context(NamedTuple):
"""The type checking context."""

globals: Globals
locals: Locals[str, Variable]
generic_param_inst: dict[str, Argument]

"""If not None, the effect constraints that function calls in this context must
respect, together with the AST node that gives rise to said constraint"""
max_effects_from: EffectLimitDecl | None = None

@property
def parsing_ctx(self) -> "TypeParsingCtx":
"""A type parsing context derived from this checking context."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, ClassVar

from guppylang_internals.diagnostic import Error, Help, Note
from guppylang_internals.tys import Effect

if TYPE_CHECKING:
from guppylang_internals.definition.util import CheckedField
Expand Down Expand Up @@ -68,6 +69,39 @@ class ConstMismatchError(Error):
actual: Const


@dataclass(frozen=True)
class TooManyEffectsError(Error):
title: ClassVar[str] = "Too many effects"
span_label: ClassVar[str] = "{target} not allowed inside `{in_func}`"
callee: str | FunctionType
effects: list[Effect]
in_func: str

@property
def target(self) -> str:
if isinstance(self.callee, str):
return f"Call to `{self.callee}`" + self.note_effects()
msg = f"Callee of type `{self.callee}`"
if self.callee.declared_effects is None:
# FunctionType that will not display any effects, so list separately
msg += self.note_effects()
return msg

def note_effects(self) -> str:
return f" has effects `{Effect.format_list(self.effects)}`"

@dataclass(frozen=True)
class MaxFromDecl(Note):
span_label: ClassVar[str] = "Allowed effects {allowed_effects_str}declared here"
allowed_effects: list[Effect] | None

@property
def allowed_effects_str(self) -> str:
if self.allowed_effects is None:
return ""
return "`" + Effect.format_list(self.allowed_effects) + "` "


@dataclass(frozen=True)
class AssignFieldTypeMismatchError(Error):
title: ClassVar[str] = "Type mismatch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
NonLinearInstantiateError,
NotCallableError,
ParameterInferenceError,
TooManyEffectsError,
TupleIndexOutOfBoundsError,
TypeApplyNotGenericError,
TypeInferenceError,
Expand Down Expand Up @@ -1327,6 +1328,32 @@ def check_comptime_arg(
return subst


def _check_effects(func_ty: FunctionType, ctx: Context, node: AstNode) -> None:
"""Checks that a function call (AST provided) to a specified FunctionType
respects the effect constraints in the context."""
if (mf := ctx.max_effects_from) is None:
return
surplus_effects = [e for e in func_ty.effects if e not in mf.effects]
if surplus_effects:
loc_node = node.func if isinstance(node, ast.Call) else node
show_effects_allowed = mf.effects
if isinstance(mf.decl, ast.expr):
# We found the decorator that is the source of the effect constraint,
# which will contain the allowed effects as an explicit argument
show_effects_allowed = None
# Otherwise, the error message points at all decorators, which may or may not
# list the allowed effects, so list them explicitly

callee = loc_node.id if isinstance(loc_node, ast.Name) else func_ty

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.

Instead of determining the callee this way, maybe accept an optional DefId to _check_effects? Or the CallableDef directly? I guess it would need to be threaded through synthesize_call though :/

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.

Yes, this is significantly better, but is quite a change to check_call / synthesize_call. I've raised #1846 for this...

raise GuppyTypeError(
TooManyEffectsError(
loc_node, callee, surplus_effects, mf.decl_name
).add_sub_diagnostic(
TooManyEffectsError.MaxFromDecl(mf.decl, show_effects_allowed)
)
)


def synthesize_call(
func_ty: FunctionType, args: list[ast.expr], node: AstNode, ctx: Context
) -> tuple[list[ast.expr], Type, Inst]:
Expand Down Expand Up @@ -1357,7 +1384,9 @@ def synthesize_call(
inst = check_all_solved(subst, free_vars, func_ty, node)

# Finally, check that the instantiation respects the linearity requirements
# and the effects allowed in the context.
check_inst(func_ty, inst, node)
_check_effects(func_ty, ctx, node)

return args, unquantified.output.substitute(subst), inst

Expand Down Expand Up @@ -1443,7 +1472,9 @@ def check_call(
subst = {v: t for v, t in subst.items() if v in ty.unsolved_vars}

# Finally, check that the instantiation respects the linearity requirements
# and the effects allowed in the context.
check_inst(func_ty, inst, node)
_check_effects(func_ty, ctx, node)

return inputs, subst, inst

Expand Down Expand Up @@ -1565,7 +1596,12 @@ def check_generator(
# The rest is checked in a new nested context to ensure that variables don't escape
# their scope
inner_locals: Locals[str, Variable] = Locals({}, parent_scope=ctx.locals)
inner_ctx = Context(ctx.globals, inner_locals, ctx.generic_param_inst)
inner_ctx = Context(
ctx.globals,
inner_locals,
ctx.generic_param_inst,
ctx.max_effects_from,
)
expr_sth, stmt_chk = ExprSynthesizer(inner_ctx), StmtChecker(inner_ctx)
gen.iter, iter_ty = expr_sth.visit(gen.iter)
gen.iter = with_type(iter_ty, gen.iter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
from guppylang_internals.cfg.bb import BB
from guppylang_internals.cfg.builder import CFGBuilder
from guppylang_internals.checker.cfg_checker import CheckedCFG, check_cfg
from guppylang_internals.checker.core import Context, Globals, Place, Variable
from guppylang_internals.checker.core import (
Context,
EffectLimitDecl,
Globals,
Place,
Variable,
)
from guppylang_internals.checker.errors.generic import UnsupportedError
from guppylang_internals.checker.unitary_checker import check_invalid_under_dagger
from guppylang_internals.definition.common import DefId
Expand Down Expand Up @@ -159,7 +165,16 @@ def check_global_func_def(
generic_args = {
param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True)
}
return check_cfg(cfg, inputs, ty.output, generic_args, func_def.name, globals)
max_effects_from = EffectLimitDecl.for_def(ty, func_def)
return check_cfg(
cfg,
inputs,
ty.output,
generic_args,
func_def.name,
globals,
max_effects_from=max_effects_from,
)


def check_nested_func_def(
Expand All @@ -168,7 +183,13 @@ def check_nested_func_def(
ctx: Context,
) -> CheckedNestedFunctionDef:
"""Type checks a local (nested) function definition."""
func_ty = check_signature(func_def, ctx.globals)
# 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.
Comment on lines +186 to +189

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]

func_ty = check_signature(func_def, ctx.globals).with_effects(
None if ctx.max_effects_from is None else ctx.max_effects_from.effects
)
assert func_ty.input_names is not None

if func_ty.parametrized:
Expand Down Expand Up @@ -247,7 +268,17 @@ def check_nested_func_def(
# Otherwise, we treat it like a local name
inputs.append(Variable(func_def.name, func_def.ty, func_def))

checked_cfg = check_cfg(cfg, inputs, func_ty.output, {}, func_def.name, globals)
checked_cfg = check_cfg(
cfg,
inputs,
func_ty.output,
{},
func_def.name,
globals,
# As comment above, assume nested func has same effects as enclosing
# (hence the decl giving the effects is that of the enclosing func too).
max_effects_from=ctx.max_effects_from,
)
checked_def = CheckedNestedFunctionDef(
def_id,
checked_cfg,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from guppylang_internals.ast_util import with_loc
from guppylang_internals.cfg.bb import BB
from guppylang_internals.checker.cfg_checker import check_cfg
from guppylang_internals.checker.core import Context, Variable
from guppylang_internals.checker.core import Context, EffectLimitDecl, Variable
from guppylang_internals.checker.unitary_checker import check_invalid_under_dagger
from guppylang_internals.definition.common import DefId
from guppylang_internals.nodes import CheckedModifiedBlock, ModifiedBlock
Expand All @@ -19,7 +19,10 @@


def check_modified_block(
modified_block: ModifiedBlock, bb: BB, ctx: Context
modified_block: ModifiedBlock,
bb: BB,
ctx: Context,
max_effects_from: EffectLimitDecl | None,
) -> CheckedModifiedBlock:
"""Type checks a modifier definition."""
cfg = modified_block.cfg
Expand Down Expand Up @@ -53,6 +56,7 @@ def check_modified_block(
{},
"__modified__()",
globals,
max_effects_from=max_effects_from,
# We pass the first modifier node for better error messages in the cfg checker
first_modifier_node=modified_block.first_modifier_node,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,9 @@ def visit_ModifiedBlock(self, node: ModifiedBlock) -> ast.stmt:
raise InternalGuppyError("BB required to check with block!")

# check the body of the modified block
checked_modified_block = check_modified_block(node, self.bb, self.ctx)
checked_modified_block = check_modified_block(
node, self.bb, self.ctx, max_effects_from=self.ctx.max_effects_from
)

# check the arguments of the control and power.
for control in checked_modified_block.control:
Expand Down
Loading
Loading