From 66a397e01a7df025043d346bef90625ca05d3250 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 4 May 2026 10:42:03 +0100 Subject: [PATCH 01/81] Decorator add max_effects as list[str] --- guppylang/src/guppylang/decorator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 08c2f8593..bbc4f2925 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -98,6 +98,7 @@ class GuppyKwargs(TypedDict, total=False): power: bool max_qubits: int link_name: str + max_effects: list[str] class GuppyStructKwargs(TypedDict, total=False): @@ -760,6 +761,9 @@ def _with_optional_kwargs( class ParsedGuppyKwargs(NamedTuple): flags: UnitaryFlags metadata: FunctionMetadata + # The empty list means no effects, whereas None means unspecified - i.e. assume all + # effects are possible until we can analyse the call-graph to calculate exactly. + max_effects: list[str] | None link_name: str | None @@ -783,6 +787,7 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: metadata.set_max_qubits(kwargs.pop("max_qubits")) link_name = kwargs.pop("link_name", None) + max_effects = kwargs.pop("max_effects", None) if remaining := next(iter(kwargs), None): err = f"Unknown keyword argument: `{remaining}`" @@ -792,6 +797,7 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: flags=flags, metadata=metadata, link_name=link_name, + max_effects=max_effects, ) From d5815fc6ffc62ef09cbe24fffebcc541d850b725 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 4 May 2026 10:57:59 +0100 Subject: [PATCH 02/81] Make annotation be list[Effect] with empty Enum --- guppylang/src/guppylang/decorator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index bbc4f2925..d437e7948 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -2,6 +2,7 @@ import builtins import inspect from collections.abc import Callable, Sequence +from enum import Enum from types import FrameType from typing import Any, NamedTuple, ParamSpec, TypedDict, TypeVar, cast, overload @@ -87,6 +88,11 @@ __all__ = ("GuppyKwargs", "custom_guppy_decorator", "guppy") +class Effect(Enum): + # No instances yet. + names = () + + class GuppyKwargs(TypedDict, total=False): """Typed dictionary specifying the optional keyword arguments for the `@guppy` decorator. @@ -98,7 +104,7 @@ class GuppyKwargs(TypedDict, total=False): power: bool max_qubits: int link_name: str - max_effects: list[str] + max_effects: list[Effect] class GuppyStructKwargs(TypedDict, total=False): @@ -763,7 +769,7 @@ class ParsedGuppyKwargs(NamedTuple): metadata: FunctionMetadata # The empty list means no effects, whereas None means unspecified - i.e. assume all # effects are possible until we can analyse the call-graph to calculate exactly. - max_effects: list[str] | None + max_effects: list[Effect] | None link_name: str | None From b28cdbd66ff4f26448690b5ee1f8145da79c9497 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 4 May 2026 11:14:50 +0100 Subject: [PATCH 03/81] Add field to Raw(Traced)FunctionDef,Decl, store string values in ParsedGuppyKwargs --- .../guppylang_internals/definition/declaration.py | 2 ++ .../src/guppylang_internals/definition/function.py | 2 ++ .../src/guppylang_internals/definition/traced.py | 2 ++ guppylang/src/guppylang/decorator.py | 12 ++++++++++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index b97712cf9..1649dbf62 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -92,6 +92,8 @@ class RawFunctionDecl(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) + max_effects: list[str] | None = field(default=None, kw_only=True) + def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": """Parses and checks the user-provided signature of the function.""" func_ast, docstring = parse_py_func(self.python_func, sources) diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 93a2b1291..5ad7a13a8 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -117,6 +117,8 @@ class RawFunctionDef(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) + max_effects: list[str] | None = field(default=None, kw_only=True) + def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": """Parses and checks the user-provided signature of the function.""" func_ast, docstring = parse_py_func(self.python_func, sources) diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index cdeeff9ed..343fe4e70 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -55,6 +55,8 @@ class RawTracedFunctionDef(ParsableDef): metadata: FunctionMetadata | None = field(default=None, kw_only=True) + max_effects: list[str] | None = field(default=None, kw_only=True) + def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": """Parses and checks the user-provided signature of the function.""" func_ast, _docstring = parse_py_func(self.python_func, sources) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index d437e7948..61fbe6be0 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -152,6 +152,7 @@ def decorator( unitary_flags=parsed.flags, metadata=parsed.metadata, link_name=parsed.link_name, + max_effects=parsed.max_effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -197,6 +198,7 @@ def decorator( f, unitary_flags=parsed.flags, metadata=parsed.metadata, + max_effects=parsed.max_effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -410,6 +412,7 @@ def decorator( unitary_flags=parsed.flags, link_name=parsed.link_name, metadata=parsed.metadata, + max_effects=parsed.max_effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -769,7 +772,7 @@ class ParsedGuppyKwargs(NamedTuple): metadata: FunctionMetadata # The empty list means no effects, whereas None means unspecified - i.e. assume all # effects are possible until we can analyse the call-graph to calculate exactly. - max_effects: list[Effect] | None + max_effects: list[str] | None link_name: str | None @@ -793,7 +796,12 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: metadata.set_max_qubits(kwargs.pop("max_qubits")) link_name = kwargs.pop("link_name", None) - max_effects = kwargs.pop("max_effects", None) + max_effects_input = kwargs.pop("max_effects", None) + max_effects = ( + None + if max_effects_input is None + else [effect._name_ for effect in max_effects_input] + ) if remaining := next(iter(kwargs), None): err = f"Unknown keyword argument: `{remaining}`" From 96b93469cf3cce1ec177b834ef87b5870b2edc94 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 4 May 2026 11:21:00 +0100 Subject: [PATCH 04/81] Add field and copy through into Parsed/Checked/Compile variants --- .../src/guppylang_internals/definition/declaration.py | 5 +++++ .../src/guppylang_internals/definition/function.py | 5 +++++ .../src/guppylang_internals/definition/traced.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 1649dbf62..ebe70a623 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -115,6 +115,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": docstring=docstring, link_name=link_name, metadata=self.metadata, + max_effects=self.max_effects, ) @@ -139,6 +140,8 @@ class ParsedFunctionDecl(CheckableGenericDef, CallableDef): link_name: str metadata: FunctionMetadata | None = field(default=None, kw_only=True) + max_effects: list[str] | None = field(default=None, kw_only=True) + @property def params(self) -> Sequence[Parameter]: return self.ty.params @@ -154,6 +157,7 @@ def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDecl": docstring=self.docstring, link_name=mono_link_name, type_args=type_args, + max_effects=self.max_effects, ) def check_call( @@ -222,6 +226,7 @@ def compile_outer( type_args=self.type_args, declaration=node, metadata=self.metadata, + max_effects=self.max_effects, ) diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 5ad7a13a8..c5d9cd188 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -135,6 +135,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": docstring, link_name, metadata=self.metadata, + max_effects=self.max_effects, ) @@ -165,6 +166,8 @@ class ParsedFunctionDef(CheckableGenericDef, CallableDef): metadata: FunctionMetadata | None = field(default=None, kw_only=True) + max_effects: list[str] | None = field(default=None, kw_only=True) + @property def params(self) -> "Sequence[Parameter]": """Generic parameters of this function.""" @@ -184,6 +187,7 @@ def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDef": mono_link_name, cfg, metadata=self.metadata, + max_effects=self.max_effects, ) def check_call( @@ -269,6 +273,7 @@ def compile_outer( self.cfg, func_def, metadata=self.metadata, + max_effects=self.max_effects, ) diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 343fe4e70..cc3834076 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -73,6 +73,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": self.python_func, unitary_flags=self.unitary_flags, metadata=self.metadata, + max_effects=self.max_effects, ) @@ -130,6 +131,7 @@ def compile_outer( func_def, unitary_flags=self.unitary_flags, metadata=self.metadata, + max_effects=self.max_effects, ) From 9a71a832f4d236fb0c0c48a642f56f563260811f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 6 May 2026 12:31:58 +0100 Subject: [PATCH 05/81] Add max_effects parameter to many funcs, with default for StmtChecker...should this be in `Context`? --- .../checker/cfg_checker.py | 22 ++++++++++++++--- .../checker/func_checker.py | 24 +++++++++++++++++-- .../checker/modifier_checker.py | 6 ++++- .../checker/stmt_checker.py | 20 +++++++++++++--- .../definition/function.py | 8 ++++++- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 39603c6d3..f72a6e9a8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -76,6 +76,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, + max_effects: list[str] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -99,7 +100,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=max_effects, ) compiled = {cfg.entry_bb: checked_cfg.entry_bb} @@ -126,7 +133,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=max_effects, ) queue += [ # We enumerate the successor starting from the back, so we start with @@ -218,6 +231,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, + max_effects: list[str] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg @@ -235,7 +249,9 @@ def check_bb( # Check the basic block ctx = Context(globals, Locals({v.name: v for v in inputs}), generic_args) - checked_stmts = StmtChecker(ctx, bb, return_ty).check_stmts(bb.statements) + checked_stmts = StmtChecker( + ctx, bb, return_ty, max_effects=max_effects + ).check_stmts(bb.statements) # If we branch, we also have to check the branch predicate if len(bb.successors) > 1: diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 338d1e95c..ad7fbab81 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -137,6 +137,7 @@ def check_global_func_def( generic_ty: FunctionType, type_args: Inst, globals: Globals, + max_effects: list[str] | None, ) -> CheckedCFG[Place]: """Type checks a top-level function definition.""" ty = generic_ty.instantiate(type_args) @@ -156,13 +157,22 @@ 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) + return check_cfg( + cfg, + inputs, + ty.output, + generic_args, + func_def.name, + globals, + max_effects=max_effects, + ) def check_nested_func_def( func_def: NestedFunctionDef, bb: BB, ctx: Context, + max_effects: list[str] | None, ) -> CheckedNestedFunctionDef: """Type checks a local (nested) function definition.""" func_ty = check_signature(func_def, ctx.globals) @@ -236,6 +246,7 @@ def check_nested_func_def( func_ty, None, link_name, + max_effects=max_effects, ) DEF_STORE.register_def(func, parent_frame) ENGINE.parsed[def_id] = func @@ -244,7 +255,15 @@ 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, + max_effects=max_effects, + ) checked_def = CheckedNestedFunctionDef( def_id, checked_cfg, @@ -268,6 +287,7 @@ def check_nested_func_def( func_def.docstring, link_name, checked_cfg, + max_effects=max_effects, ) return with_loc(func_def, checked_def) diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index 26439bd72..d00d1aae0 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -20,7 +20,10 @@ def check_modified_block( - modified_block: ModifiedBlock, bb: BB, ctx: Context + modified_block: ModifiedBlock, + bb: BB, + ctx: Context, + max_effects: list[str] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg @@ -73,6 +76,7 @@ def check_modified_block( {}, "__modified__()", globals, + max_effects=max_effects, # We pass the first modifier node for better error messages in the cfg checker first_modifier_node=modified_block.first_modifier_node, ) diff --git a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py index 4628d3577..0237a4a73 100644 --- a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py @@ -101,14 +101,20 @@ class StmtChecker(AstVisitor[BBStatement]): ctx: Context bb: BB | None return_ty: Type | None + max_effects: list[str] | None def __init__( - self, ctx: Context, bb: BB | None = None, return_ty: Type | None = None + self, + ctx: Context, + bb: BB | None = None, + return_ty: Type | None = None, + max_effects: list[str] | None = None, ) -> None: assert not return_ty or not return_ty.unsolved_vars self.ctx = ctx self.bb = bb self.return_ty = return_ty + self.max_effects = max_effects def check_stmts(self, stmts: Sequence[BBStatement]) -> list[BBStatement]: return [self.visit(s) for s in stmts] @@ -409,7 +415,13 @@ def visit_NestedFunctionDef(self, node: NestedFunctionDef) -> ast.stmt: if not self.bb: raise InternalGuppyError("BB required to check nested function def!") - func_def = check_nested_func_def(node, self.bb, self.ctx) + # 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. + func_def = check_nested_func_def( + node, self.bb, self.ctx, max_effects=self.max_effects + ) self.ctx.locals[func_def.name] = Variable(func_def.name, func_def.ty, func_def) return func_def @@ -420,7 +432,9 @@ def visit_ModifiedBlock(self, node: ModifiedBlock) -> ast.stmt: raise InternalGuppyError("BB required to check with block!") # check the body of the modified block - modified_block = check_modified_block(node, self.bb, self.ctx) + modified_block = check_modified_block( + node, self.bb, self.ctx, max_effects=self.max_effects + ) # check the arguments of the control and power. for control in modified_block.control: diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index c5d9cd188..987688889 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -175,7 +175,13 @@ def params(self) -> "Sequence[Parameter]": def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDef": """Type checks the body of the function.""" - cfg = check_global_func_def(self.defined_at, self.ty, type_args, globals) + cfg = check_global_func_def( + self.defined_at, + self.ty, + type_args, + globals, + max_effects=self.max_effects, + ) mono_ty = self.ty.instantiate_partial(type_args) mono_link_name = monomorphized_link_name(self.link_name, type_args) return CheckedFunctionDef( From d8632ba0021d08e64ec15f6e13d61b659712a7b0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 6 May 2026 19:45:10 +0100 Subject: [PATCH 06/81] Remove from StmtChecker, add to Context --- .../src/guppylang_internals/checker/cfg_checker.py | 8 ++++---- .../src/guppylang_internals/checker/core.py | 1 + .../src/guppylang_internals/checker/expr_checker.py | 7 ++++++- .../src/guppylang_internals/checker/stmt_checker.py | 7 ++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index f72a6e9a8..06bf13294 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -248,10 +248,10 @@ def check_bb( raise GuppyError(VarNotDefinedError(use, x)) # Check the basic block - ctx = Context(globals, Locals({v.name: v for v in inputs}), generic_args) - checked_stmts = StmtChecker( - ctx, bb, return_ty, max_effects=max_effects - ).check_stmts(bb.statements) + ctx = Context( + globals, Locals({v.name: v for v in inputs}), generic_args, max_effects + ) + checked_stmts = StmtChecker(ctx, bb, return_ty).check_stmts(bb.statements) # If we branch, we also have to check the branch predicate if len(bb.successors) > 1: diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index 8b43c027d..e9b1a2787 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -447,6 +447,7 @@ class Context(NamedTuple): globals: Globals locals: Locals[str, Variable] generic_param_inst: dict[str, Argument] + max_effects: list[str] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 2f1b6ec32..97c4e452c 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1479,7 +1479,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, + ) 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) diff --git a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py index 0237a4a73..843a4db31 100644 --- a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py @@ -101,20 +101,17 @@ class StmtChecker(AstVisitor[BBStatement]): ctx: Context bb: BB | None return_ty: Type | None - max_effects: list[str] | None def __init__( self, ctx: Context, bb: BB | None = None, return_ty: Type | None = None, - max_effects: list[str] | None = None, ) -> None: assert not return_ty or not return_ty.unsolved_vars self.ctx = ctx self.bb = bb self.return_ty = return_ty - self.max_effects = max_effects def check_stmts(self, stmts: Sequence[BBStatement]) -> list[BBStatement]: return [self.visit(s) for s in stmts] @@ -420,7 +417,7 @@ def visit_NestedFunctionDef(self, node: NestedFunctionDef) -> ast.stmt: # 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. func_def = check_nested_func_def( - node, self.bb, self.ctx, max_effects=self.max_effects + node, self.bb, self.ctx, max_effects=self.ctx.max_effects ) self.ctx.locals[func_def.name] = Variable(func_def.name, func_def.ty, func_def) return func_def @@ -433,7 +430,7 @@ def visit_ModifiedBlock(self, node: ModifiedBlock) -> ast.stmt: # check the body of the modified block modified_block = check_modified_block( - node, self.bb, self.ctx, max_effects=self.max_effects + node, self.bb, self.ctx, max_effects=self.ctx.max_effects ) # check the arguments of the control and power. From 2afa232ea7e122bac13a4f9b7d0851c95f154ec5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 6 May 2026 21:22:25 +0100 Subject: [PATCH 07/81] driveby common-up FunctionType constructions in decorator.py --- .../src/guppylang_internals/decorator.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index fccc406a4..df01b394c 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -344,18 +344,17 @@ def dec(cls: builtins.type[T]) -> GuppyDefinition: ) # Add a constructor to the class - if init_arg: - init_fn_ty = FunctionType( - [ - FuncInput( - NumericType(NumericType.Kind.Nat), - flags=InputFlags.Owned, - ) - ], - ext_module_ty, - ) - else: - init_fn_ty = FunctionType([], ext_module_ty) + init_fn_ty = FunctionType( + [ + FuncInput( + NumericType(NumericType.Kind.Nat), + flags=InputFlags.Owned, + ) + ] + if init_arg + else [], + ext_module_ty, + ) call_method = CustomFunctionDef( DefId.fresh(), From 4d256e5bf3294f8953b654551e19ac908b8e38f6 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 6 May 2026 21:52:35 +0100 Subject: [PATCH 08/81] Remove from Parsed def/decl (only Raw), add to FunctionType - still in Context and check_bb/cfg/etc. --- .../checker/func_checker.py | 7 ++--- .../definition/declaration.py | 7 +---- .../definition/function.py | 8 +----- .../src/guppylang_internals/tys/ty.py | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index ad7fbab81..3bafa3142 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -137,7 +137,6 @@ def check_global_func_def( generic_ty: FunctionType, type_args: Inst, globals: Globals, - max_effects: list[str] | None, ) -> CheckedCFG[Place]: """Type checks a top-level function definition.""" ty = generic_ty.instantiate(type_args) @@ -164,7 +163,7 @@ def check_global_func_def( generic_args, func_def.name, globals, - max_effects=max_effects, + max_effects=ty.max_effects, ) @@ -175,7 +174,7 @@ def check_nested_func_def( max_effects: list[str] | None, ) -> CheckedNestedFunctionDef: """Type checks a local (nested) function definition.""" - func_ty = check_signature(func_def, ctx.globals) + func_ty = check_signature(func_def, ctx.globals).with_effects(max_effects) assert func_ty.input_names is not None if func_ty.parametrized: @@ -246,7 +245,6 @@ def check_nested_func_def( func_ty, None, link_name, - max_effects=max_effects, ) DEF_STORE.register_def(func, parent_frame) ENGINE.parsed[def_id] = func @@ -287,7 +285,6 @@ def check_nested_func_def( func_def.docstring, link_name, checked_cfg, - max_effects=max_effects, ) return with_loc(func_def, checked_def) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index ebe70a623..39946b486 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -99,7 +99,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature( func_ast, globals, self.id, unitary_flags=self.unitary_flags - ) + ).with_effects(self.max_effects) link_name = self._user_set_link_name or default_func_link_name(self) # TODO: For the guppylang 1.0 break, we should consider disallowing generic @@ -115,7 +115,6 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": docstring=docstring, link_name=link_name, metadata=self.metadata, - max_effects=self.max_effects, ) @@ -140,8 +139,6 @@ class ParsedFunctionDecl(CheckableGenericDef, CallableDef): link_name: str metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[str] | None = field(default=None, kw_only=True) - @property def params(self) -> Sequence[Parameter]: return self.ty.params @@ -157,7 +154,6 @@ def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDecl": docstring=self.docstring, link_name=mono_link_name, type_args=type_args, - max_effects=self.max_effects, ) def check_call( @@ -226,7 +222,6 @@ def compile_outer( type_args=self.type_args, declaration=node, metadata=self.metadata, - max_effects=self.max_effects, ) diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 987688889..5229e185a 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -124,7 +124,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature( func_ast, globals, self.id, unitary_flags=self.unitary_flags - ) + ).with_effects(self.max_effects) link_name = self._user_set_link_name or default_func_link_name(self) return ParsedFunctionDef( @@ -135,7 +135,6 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": docstring, link_name, metadata=self.metadata, - max_effects=self.max_effects, ) @@ -166,8 +165,6 @@ class ParsedFunctionDef(CheckableGenericDef, CallableDef): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[str] | None = field(default=None, kw_only=True) - @property def params(self) -> "Sequence[Parameter]": """Generic parameters of this function.""" @@ -180,7 +177,6 @@ def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDef": self.ty, type_args, globals, - max_effects=self.max_effects, ) mono_ty = self.ty.instantiate_partial(type_args) mono_link_name = monomorphized_link_name(self.link_name, type_args) @@ -193,7 +189,6 @@ def check(self, type_args: Inst, globals: Globals) -> "CheckedFunctionDef": mono_link_name, cfg, metadata=self.metadata, - max_effects=self.max_effects, ) def check_call( @@ -279,7 +274,6 @@ def compile_outer( self.cfg, func_def, metadata=self.metadata, - max_effects=self.max_effects, ) diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index 38d86840c..d561d8a15 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -413,6 +413,8 @@ class FunctionType(ParametrizedTypeBase): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, init=True) + max_effects: list[str] | None = field(default=None, init=True) + def __init__( self, inputs: Sequence[FuncInput], @@ -420,6 +422,7 @@ def __init__( params: Sequence[Parameter] | None = None, comptime_args: Sequence[ConstArg] | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, + max_effects: list[str] | None = None, ) -> None: # We need a custom __init__ to set the args args: list[Argument] = [TypeArg(inp.ty) for inp in inputs] @@ -448,6 +451,7 @@ def __init__( object.__setattr__(self, "output", output) object.__setattr__(self, "params", params) object.__setattr__(self, "unitary_flags", unitary_flags) + object.__setattr__(self, "max_effects", max_effects) @property def parametrized(self) -> bool: @@ -501,6 +505,8 @@ def _to_hugr_function_type(self, ctx: ToHugrContext) -> ht.FunctionType: The resulting `FunctionType` can then be embedded into a Hugr `Type` or a Hugr `PolyFuncType`. """ + # At some point we may want to represent the max_effects as input and + # perhaps output "token" types in Hugr, but for now we will use Order edges. ins = [ inp.ty.to_hugr(ctx) for inp in self.inputs @@ -535,6 +541,7 @@ def transform(self, transformer: Transformer) -> "Type": self.params, comptime_args=self.comptime_args, unitary_flags=self.unitary_flags, + max_effects=self.max_effects, ) def instantiate_partial(self, args: "PartialInst") -> "FunctionType": @@ -564,6 +571,7 @@ def instantiate_partial(self, args: "PartialInst") -> "FunctionType": cast("ConstArg", arg.transform(inst)) for arg in self.comptime_args ], unitary_flags=self.unitary_flags, + max_effects=self.max_effects, ) def instantiate(self, args: "Inst") -> "FunctionType": @@ -594,6 +602,24 @@ def with_unitary_flags(self, flags: UnitaryFlags) -> "FunctionType": self.params, self.comptime_args, flags, + max_effects=self.max_effects, + ) + + def with_effects(self, max_effects: list[str] | None) -> "FunctionType": + """Returns a copy of this function type with the specified max_effects.""" + # N.B. we can't use `dataclasses.replace` here since `FunctionType` has a custom + # constructor + if self.max_effects is not None: + raise InternalGuppyError( + "Tried to set max_effects on a FunctionType that already has them" + ) + return FunctionType( + self.inputs, + self.output, + self.params, + self.comptime_args, + self.unitary_flags, + max_effects=max_effects, ) From 706f062c4bd6aed2bcf18dfa44022b3bc4cd237b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:35:02 +0100 Subject: [PATCH 09/81] check_nested_func_def gets max_effects from Context --- .../src/guppylang_internals/checker/func_checker.py | 9 ++++++--- .../src/guppylang_internals/checker/stmt_checker.py | 13 ++----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 3bafa3142..22b6f1df7 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -171,10 +171,13 @@ def check_nested_func_def( func_def: NestedFunctionDef, bb: BB, ctx: Context, - max_effects: list[str] | None, ) -> CheckedNestedFunctionDef: """Type checks a local (nested) function definition.""" - func_ty = check_signature(func_def, ctx.globals).with_effects(max_effects) + # 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. + func_ty = check_signature(func_def, ctx.globals).with_effects(ctx.max_effects) assert func_ty.input_names is not None if func_ty.parametrized: @@ -260,7 +263,7 @@ def check_nested_func_def( {}, func_def.name, globals, - max_effects=max_effects, + max_effects=func_ty.max_effects, ) checked_def = CheckedNestedFunctionDef( def_id, diff --git a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py index 843a4db31..447deb010 100644 --- a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py @@ -103,10 +103,7 @@ class StmtChecker(AstVisitor[BBStatement]): return_ty: Type | None def __init__( - self, - ctx: Context, - bb: BB | None = None, - return_ty: Type | None = None, + self, ctx: Context, bb: BB | None = None, return_ty: Type | None = None ) -> None: assert not return_ty or not return_ty.unsolved_vars self.ctx = ctx @@ -412,13 +409,7 @@ def visit_NestedFunctionDef(self, node: NestedFunctionDef) -> ast.stmt: if not self.bb: raise InternalGuppyError("BB required to check nested function def!") - # 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. - func_def = check_nested_func_def( - node, self.bb, self.ctx, max_effects=self.ctx.max_effects - ) + func_def = check_nested_func_def(node, self.bb, self.ctx) self.ctx.locals[func_def.name] = Variable(func_def.name, func_def.ty, func_def) return func_def From 1067f5c4c7dab50ee2170014eb98e7ac5776e5e3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 6 May 2026 22:02:04 +0100 Subject: [PATCH 10/81] Add TooManyEffectsError, check+raise in toplevel check_call+synthesize_call --- .../checker/errors/type_errors.py | 11 +++++++++++ .../src/guppylang_internals/checker/expr_checker.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 6b6cb7d39..1751356a7 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -48,6 +48,17 @@ class ConstMismatchError(Error): actual: Const +@dataclass(frozen=True) +class TooManyEffectsError(Error): + title: ClassVar[str] = "Too many effects" + span_label: ClassVar[str] = ( + "Callee of type `{ty}` has effects that exceed the " + "allowed effects `{allowed_effects}`" + ) + ty: Type + allowed_effects: list[str] + + @dataclass(frozen=True) class AssignFieldTypeMismatchError(Error): title: ClassVar[str] = "Type mismatch" diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 97c4e452c..b5289de40 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -81,6 +81,7 @@ NonLinearInstantiateError, NotCallableError, ParameterInferenceError, + TooManyEffectsError, TupleIndexOutOfBoundsError, TypeApplyNotGenericError, TypeInferenceError, @@ -1261,6 +1262,12 @@ def synthesize_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(args), node, func_ty) + if ctx.max_effects is not None and ( + func_ty.max_effects is None + or any(e not in ctx.max_effects for e in func_ty.max_effects) + ): + raise GuppyTypeError(TooManyEffectsError(node, func_ty, ctx.max_effects)) + # Replace quantified variables with free unification variables and try to infer an # instantiation by checking the arguments unquantified, free_vars = func_ty.unquantified() @@ -1292,6 +1299,12 @@ def check_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(inputs), node, func_ty) + if ctx.max_effects is not None and ( + func_ty.max_effects is None + or any(e not in ctx.max_effects for e in func_ty.max_effects) + ): + raise GuppyTypeError(TooManyEffectsError(node, func_ty, ctx.max_effects)) + # When checking, we can use the information from the expected return type to infer # some type arguments. However, this pushes errors inwards. For example, given a # function `foo: forall T. T -> T`, the following type mismatch would be reported: From 10b682c9cfb07762fdb748179e0120171e224d01 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:28:40 +0100 Subject: [PATCH 11/81] First success/failure tests; no binary ops yet, and error messages need notes --- .../checker/errors/type_errors.py | 9 +++- .../checker/expr_checker.py | 10 +++- .../effects_errors/pure_calls_impure_decl.err | 9 ++++ .../effects_errors/pure_calls_impure_decl.py | 11 +++++ .../effects_errors/pure_calls_impure_def.err | 9 ++++ .../effects_errors/pure_calls_impure_def.py | 13 +++++ tests/error/test_effects_errors.py | 19 +++++++ tests/integration/test_effects.py | 49 +++++++++++++++++++ 8 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 tests/error/effects_errors/pure_calls_impure_decl.err create mode 100644 tests/error/effects_errors/pure_calls_impure_decl.py create mode 100644 tests/error/effects_errors/pure_calls_impure_def.err create mode 100644 tests/error/effects_errors/pure_calls_impure_def.py create mode 100644 tests/error/test_effects_errors.py create mode 100644 tests/integration/test_effects.py diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 1751356a7..8dd7bd239 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -52,12 +52,17 @@ class ConstMismatchError(Error): class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects that exceed the " - "allowed effects `{allowed_effects}`" + "Callee of type `{ty}` has effects {effects}\n" + "that exceed the allowed effects `{allowed_effects}`" ) ty: Type + # ALAN TODO can we transform None -> str in __init__ rather than caller doing that? + effects: list[str] | str allowed_effects: list[str] + # ALAN would be good to Note both where the callee is defined + # and where the caller is declared as excluding those effects. + @dataclass(frozen=True) class AssignFieldTypeMismatchError(Error): diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index b5289de40..c238f45ab 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1266,7 +1266,10 @@ def synthesize_call( func_ty.max_effects is None or any(e not in ctx.max_effects for e in func_ty.max_effects) ): - raise GuppyTypeError(TooManyEffectsError(node, func_ty, ctx.max_effects)) + effects = "" if func_ty.max_effects is None else func_ty.max_effects + raise GuppyTypeError( + TooManyEffectsError(node, func_ty, effects, ctx.max_effects) + ) # Replace quantified variables with free unification variables and try to infer an # instantiation by checking the arguments @@ -1303,7 +1306,10 @@ def check_call( func_ty.max_effects is None or any(e not in ctx.max_effects for e in func_ty.max_effects) ): - raise GuppyTypeError(TooManyEffectsError(node, func_ty, ctx.max_effects)) + effects = "" if func_ty.max_effects is None else func_ty.max_effects + raise GuppyTypeError( + TooManyEffectsError(node, func_ty, effects, ctx.max_effects) + ) # When checking, we can use the information from the expected return type to infer # some type arguments. However, this pushes errors inwards. For example, given a diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err new file mode 100644 index 000000000..8af14ad91 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -0,0 +1,9 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(max_effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.py b/tests/error/effects_errors/pure_calls_impure_decl.py new file mode 100644 index 000000000..a03124f34 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_decl.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy +from guppylang.std.builtins import array + +@guppy.declare +def impure_func(x: int) -> int: ... + +@guppy(max_effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err new file mode 100644 index 000000000..312ee7670 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -0,0 +1,9 @@ +Error: Too many effects (at $FILE:11:10) + | + 9 | @guppy(max_effects=[]) +10 | def main() -> int: +11 | return impure_func(5) + | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.py b/tests/error/effects_errors/pure_calls_impure_def.py new file mode 100644 index 000000000..dc691ff82 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_def.py @@ -0,0 +1,13 @@ +from guppylang.decorator import guppy +from guppylang.std.builtins import array + +@guppy +def impure_func(x: int) -> int: + #Use result, or similar? + return x + 1 + +@guppy(max_effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() \ No newline at end of file diff --git a/tests/error/test_effects_errors.py b/tests/error/test_effects_errors.py new file mode 100644 index 000000000..f97f2ca19 --- /dev/null +++ b/tests/error/test_effects_errors.py @@ -0,0 +1,19 @@ +import pathlib +import pytest + +from tests.error.util import run_error_test + +path = pathlib.Path(__file__).parent.resolve() / "effects_errors" +files = [ + x + for x in path.iterdir() + if x.is_file() and x.suffix == ".py" and x.name != "__init__.py" +] + +# Turn paths into strings, otherwise pytest doesn't display the names +files = [str(f) for f in files] + + +@pytest.mark.parametrize("file", files) +def test_effects_errors(file, capsys, snapshot): + run_error_test(file, capsys, snapshot) diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py new file mode 100644 index 000000000..cbcf12cdd --- /dev/null +++ b/tests/integration/test_effects.py @@ -0,0 +1,49 @@ +"""Tests of max_effects annotation.""" + +from guppylang.decorator import guppy + + +def test_pure_decl_from_impure(validate): + @guppy.declare(max_effects=[]) + def pure_func(x: int) -> int: ... + + @guppy + def impure_func(x: int) -> int: + return pure_func(x) + 1 + + validate(impure_func.compile_function()) + + +def test_pure_decl_from_pure(validate): + @guppy.declare(max_effects=[]) + def pure_func1(x: int) -> int: ... + + @guppy(max_effects=[]) + def pure_func2(x: int) -> int: + return pure_func1(pure_func1(x)) + + validate(pure_func2.compile_function()) + + +def test_pure_from_impure(validate): + @guppy(max_effects=[]) + def pure_func(x: int) -> int: + return x + + @guppy + def normal_func(x: int) -> int: + return pure_func(x) + 1 + + validate(normal_func.compile_function()) + + +def test_pure_from_pure(validate): + @guppy(max_effects=[]) + def pure_func1(x: int) -> int: + return x + + @guppy(max_effects=[]) + def pure_func2(x: int) -> int: + return pure_func1(pure_func1(x)) + + validate(pure_func2.compile_function()) From 6f92edbc82cc70df9524a20d7b4bd73ed64ae806 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:44:29 +0100 Subject: [PATCH 12/81] Add test of calling result...aaiieeeee --- tests/error/effects_errors/pure_result.err | 19 +++++++++++++++++++ tests/error/effects_errors/pure_result.py | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/error/effects_errors/pure_result.err create mode 100644 tests/error/effects_errors/pure_result.py diff --git a/tests/error/effects_errors/pure_result.err b/tests/error/effects_errors/pure_result.err new file mode 100644 index 000000000..ec2dbcd14 --- /dev/null +++ b/tests/error/effects_errors/pure_result.err @@ -0,0 +1,19 @@ +Error: Invalid call of overloaded function (at $FILE:6:10) + | +4 | @guppy(max_effects=[]) +5 | def main() -> int: +6 | result("foo", True) + | ^^^^^^^^^^^ No variant of overloaded function `result` takes arguments + | `str`, `bool` + +Note: Available overloads are: + def result(tag: str @comptime, value: int) -> None + def result(tag: str @comptime, value: nat) -> None + def result(tag: str @comptime, value: bool) -> None + def result(tag: str @comptime, value: float) -> None + def result(tag: str @comptime, value: array[int, n]) -> None + def result(tag: str @comptime, value: array[nat, n]) -> None + def result(tag: str @comptime, value: array[bool, n]) -> None + def result(tag: str @comptime, value: array[float, n]) -> None + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_result.py b/tests/error/effects_errors/pure_result.py new file mode 100644 index 000000000..daf1827b8 --- /dev/null +++ b/tests/error/effects_errors/pure_result.py @@ -0,0 +1,9 @@ +from guppylang.decorator import guppy +from guppylang.std.builtins import result + +@guppy(max_effects=[]) +def main() -> int: + result("foo", True) + return 3 + +main.compile_function() \ No newline at end of file From 595e0bbb096c05eb44a105bf2b1e8e570f02cae6 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:54:57 +0100 Subject: [PATCH 13/81] Add max_effects to @hugr_op and RawCustomFunctionDef --- guppylang-internals/src/guppylang_internals/decorator.py | 4 ++++ .../src/guppylang_internals/definition/custom.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index df01b394c..f9d905c2d 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -88,6 +88,7 @@ def custom_function( signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, has_var_args: bool = False, + max_effects: list[str] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to add custom typing or compilation behaviour to function decls. @@ -112,6 +113,7 @@ def dec(f: Callable[P, T]) -> GuppyFunctionDefinition[P, T]: signature, unitary_flags, has_var_args, + max_effects=max_effects, ) DEF_STORE.register_def(func, get_calling_frame()) return GuppyFunctionDefinition(func) @@ -126,6 +128,7 @@ def hugr_op( name: str = "", signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, + max_effects: list[str] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to annotate function declarations as HUGR ops. @@ -144,6 +147,7 @@ def hugr_op( name, signature, unitary_flags=unitary_flags, + max_effects=max_effects, ) diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index b7a6f8499..ca6551d83 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -124,6 +124,8 @@ class RawCustomFunctionDef(ParsableDef): # in Guppy functions in general but some custom functions make use of them). has_var_args: bool = field(default=False) + max_effects: list[str] | None = field(default=None) + description: str = field(default="function", init=False) def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef": @@ -146,7 +148,7 @@ def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef": raise GuppyError(BodyNotEmptyError(func_ast.body[0], self.name)) sig = self.signature or self._get_signature(func_ast, globals) ty = sig or FunctionType([], NoneType()) - ty = ty.with_unitary_flags(self.unitary_flags) + ty = ty.with_unitary_flags(self.unitary_flags).with_effects(self.max_effects) return CustomFunctionDef( self.id, self.name, From c1b643936ee957782babd1998bf14a74498b94b2 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:57:32 +0100 Subject: [PATCH 14/81] Use int+ in tests, breaking --- tests/integration/test_effects.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index cbcf12cdd..171714a3d 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -20,7 +20,7 @@ def pure_func1(x: int) -> int: ... @guppy(max_effects=[]) def pure_func2(x: int) -> int: - return pure_func1(pure_func1(x)) + return pure_func1(x) + 2 validate(pure_func2.compile_function()) @@ -28,11 +28,11 @@ def pure_func2(x: int) -> int: def test_pure_from_impure(validate): @guppy(max_effects=[]) def pure_func(x: int) -> int: - return x + return x + 1 @guppy def normal_func(x: int) -> int: - return pure_func(x) + 1 + return pure_func(x) + 2 validate(normal_func.compile_function()) @@ -40,10 +40,10 @@ def normal_func(x: int) -> int: def test_pure_from_pure(validate): @guppy(max_effects=[]) def pure_func1(x: int) -> int: - return x + return x + 1 @guppy(max_effects=[]) def pure_func2(x: int) -> int: - return pure_func1(pure_func1(x)) + return pure_func1(pure_func1(x)) + 1 validate(pure_func2.compile_function()) From dd1245d834d10de4f33b52217e65ee273f831d60 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 7 May 2026 18:57:48 +0100 Subject: [PATCH 15/81] Declare int+ as having no effects...fixes tests --- guppylang/src/guppylang/std/num.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 631abd7c7..1ab6bdf3c 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -171,7 +171,7 @@ class int: @hugr_op(int_op("iabs")) # TODO: Maybe wrong? (signed vs unsigned!) def __abs__(self: int) -> int: ... - @hugr_op(int_op("iadd")) + @hugr_op(int_op("iadd"), max_effects=[]) def __add__(self: int, other: int) -> int: ... @hugr_op(int_op("iand")) From 28454b49775da4fdd7a67efc20b0971b9575ba16 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 11:33:58 +0100 Subject: [PATCH 16/81] test cleanups (don't import array), inc driveby bracket-removal --- tests/error/effects_errors/pure_calls_impure_decl.err | 8 ++++---- tests/error/effects_errors/pure_calls_impure_decl.py | 1 - tests/error/effects_errors/pure_calls_impure_def.err | 8 ++++---- tests/error/effects_errors/pure_calls_impure_def.py | 1 - tests/integration/test_enum.py | 9 +++------ 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 8af14ad91..3c2772866 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -1,8 +1,8 @@ -Error: Too many effects (at $FILE:9:10) +Error: Too many effects (at $FILE:8:10) | -7 | @guppy(max_effects=[]) -8 | def main() -> int: -9 | return impure_func(5) +6 | @guppy(max_effects=[]) +7 | def main() -> int: +8 | return impure_func(5) | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects | that exceed the allowed effects `[]` diff --git a/tests/error/effects_errors/pure_calls_impure_decl.py b/tests/error/effects_errors/pure_calls_impure_decl.py index a03124f34..6ca19211d 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.py +++ b/tests/error/effects_errors/pure_calls_impure_decl.py @@ -1,5 +1,4 @@ from guppylang.decorator import guppy -from guppylang.std.builtins import array @guppy.declare def impure_func(x: int) -> int: ... diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 312ee7670..0a086037a 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -1,8 +1,8 @@ -Error: Too many effects (at $FILE:11:10) +Error: Too many effects (at $FILE:10:10) | - 9 | @guppy(max_effects=[]) -10 | def main() -> int: -11 | return impure_func(5) + 8 | @guppy(max_effects=[]) + 9 | def main() -> int: +10 | return impure_func(5) | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects | that exceed the allowed effects `[]` diff --git a/tests/error/effects_errors/pure_calls_impure_def.py b/tests/error/effects_errors/pure_calls_impure_def.py index dc691ff82..97dfc68a0 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.py +++ b/tests/error/effects_errors/pure_calls_impure_def.py @@ -1,5 +1,4 @@ from guppylang.decorator import guppy -from guppylang.std.builtins import array @guppy def impure_func(x: int) -> int: diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index 92fedd5b9..b302ba621 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -23,11 +23,8 @@ from guppylang import guppy from tests.util import compile_guppy -from typing import Generic, TYPE_CHECKING - - -if TYPE_CHECKING: - from collections.abc import Callable +from typing import Generic +from collections.abc import Callable def test_basic_enum(validate): @@ -250,7 +247,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: "Callable[[int], Enum[int]]", x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int]], x: int) -> Enum[int]: return mk_enum(x) @guppy From be231e032785394a4a1df1ee263486fb01f16ee3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 11:34:22 +0100 Subject: [PATCH 17/81] Add error test: Callable has unknown effects --- .../error/effects_errors/pure_calls_impure_callable.err | 9 +++++++++ tests/error/effects_errors/pure_calls_impure_callable.py | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/error/effects_errors/pure_calls_impure_callable.err create mode 100644 tests/error/effects_errors/pure_calls_impure_callable.py diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err new file mode 100644 index 000000000..8cec52fe7 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -0,0 +1,9 @@ +Error: Too many effects (at $FILE:5:10) + | +3 | @guppy(max_effects=[]) +4 | def main(impure_f: Callable[[int], int]) -> int: +5 | return impure_f(5) + | ^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable.py b/tests/error/effects_errors/pure_calls_impure_callable.py new file mode 100644 index 000000000..ae1ef9fd5 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_callable.py @@ -0,0 +1,7 @@ +from guppylang.decorator import guppy + +@guppy(max_effects=[]) +def main(impure_f: Callable[[int], int]) -> int: + return impure_f(5) + +main.compile() \ No newline at end of file From 6ee59a3b6375de8f0e07d10dd2a4f30cd71bf872 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 11:43:08 +0100 Subject: [PATCH 18/81] refactor: Common up effects check --- .../checker/errors/type_errors.py | 1 - .../checker/expr_checker.py | 31 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 8dd7bd239..c6ee19705 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -56,7 +56,6 @@ class TooManyEffectsError(Error): "that exceed the allowed effects `{allowed_effects}`" ) ty: Type - # ALAN TODO can we transform None -> str in __init__ rather than caller doing that? effects: list[str] | str allowed_effects: list[str] diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index c238f45ab..0b411ee6c 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1251,6 +1251,19 @@ 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 ctx.max_effects is not None and ( + func_ty.max_effects is None + or any(e not in ctx.max_effects for e in func_ty.max_effects) + ): + effects = "" if func_ty.max_effects is None else func_ty.max_effects + raise GuppyTypeError( + TooManyEffectsError(node, func_ty, effects, ctx.max_effects) + ) + + def synthesize_call( func_ty: FunctionType, args: list[ast.expr], node: AstNode, ctx: Context ) -> tuple[list[ast.expr], Type, Inst]: @@ -1262,14 +1275,7 @@ def synthesize_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(args), node, func_ty) - if ctx.max_effects is not None and ( - func_ty.max_effects is None - or any(e not in ctx.max_effects for e in func_ty.max_effects) - ): - effects = "" if func_ty.max_effects is None else func_ty.max_effects - raise GuppyTypeError( - TooManyEffectsError(node, func_ty, effects, ctx.max_effects) - ) + _check_effects(func_ty, ctx, node) # Replace quantified variables with free unification variables and try to infer an # instantiation by checking the arguments @@ -1302,14 +1308,7 @@ def check_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(inputs), node, func_ty) - if ctx.max_effects is not None and ( - func_ty.max_effects is None - or any(e not in ctx.max_effects for e in func_ty.max_effects) - ): - effects = "" if func_ty.max_effects is None else func_ty.max_effects - raise GuppyTypeError( - TooManyEffectsError(node, func_ty, effects, ctx.max_effects) - ) + _check_effects(func_ty, ctx, node) # When checking, we can use the information from the expected return type to infer # some type arguments. However, this pushes errors inwards. For example, given a From dbc5fcbe17ee59452ba36098b2f753e656977ff8 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 16:57:29 +0100 Subject: [PATCH 19/81] improve error location when possible --- .../src/guppylang_internals/checker/expr_checker.py | 3 ++- tests/error/effects_errors/pure_calls_impure_callable.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_decl.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_def.err | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 0b411ee6c..6af0e0ac4 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1258,9 +1258,10 @@ def _check_effects(func_ty: FunctionType, ctx: Context, node: AstNode) -> None: func_ty.max_effects is None or any(e not in ctx.max_effects for e in func_ty.max_effects) ): + loc_node = node.func if isinstance(node, ast.Call) else node effects = "" if func_ty.max_effects is None else func_ty.max_effects raise GuppyTypeError( - TooManyEffectsError(node, func_ty, effects, ctx.max_effects) + TooManyEffectsError(loc_node, func_ty, effects, ctx.max_effects) ) diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 8cec52fe7..7e958b1a4 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -3,7 +3,7 @@ Error: Too many effects (at $FILE:5:10) 3 | @guppy(max_effects=[]) 4 | def main(impure_f: Callable[[int], int]) -> int: 5 | return impure_f(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | ^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 3c2772866..5a016f346 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -3,7 +3,7 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(max_effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | ^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 0a086037a..99395cb73 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -3,7 +3,7 @@ Error: Too many effects (at $FILE:10:10) 8 | @guppy(max_effects=[]) 9 | def main() -> int: 10 | return impure_func(5) - | ^^^^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | ^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` Guppy compilation failed due to 1 previous error From 87959e6a7b6897a9d2848232fdca5edffe5bf345 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 17:29:05 +0100 Subject: [PATCH 20/81] Parse Callable[[inp,...],out,max_effects] --- .../src/guppylang_internals/tys/parsing.py | 26 ++++++++++++++++--- tests/integration/test_effects.py | 18 +++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 4b9ac5289..10ffe3b30 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -234,16 +234,34 @@ def _parse_delayed_annotation(ast_str: str, node: ast.Constant) -> ast.expr: def _parse_callable_type( args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx ) -> FunctionType: - """Helper function to parse a `Callable[[], ]` type.""" + """Helper function to parse a `Callable` type: + either `Callable[[], ]` + or `Callable[[], , ]`.""" err = InvalidCallableTypeError(loc) - if len(args) != 2: + if len(args) not in [2, 3]: raise GuppyError(err) - [inputs, output] = args + inputs = args[0] + output = args[1] if not isinstance(inputs, ast.List): raise GuppyError(err) inputs = [parse_function_arg_annotation(inp, None, ctx) for inp in inputs.elts] output = type_from_ast(output, ctx) - return FunctionType(inputs, output) + + max_effects: list[str] | None + if len(args) == 2: + max_effects = None + elif not isinstance(args[2], ast.List): + raise GuppyError(err) + else: + max_effects = [] + for e in args[2].elts: + # TODO The max_effects should be a list of `class Effect` i.e. a + # guppylang_internals version of that in guppylang.decorator. + # Then we could parse each to an element of that. + if True or (not isinstance(e, ast.Name)): # noqa: SIM222 + raise GuppyError(err) + max_effects.append(e.id) + return FunctionType(inputs, output, max_effects=max_effects) def _parse_self_type(args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx) -> Type: diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index 171714a3d..9399e8333 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -1,5 +1,7 @@ """Tests of max_effects annotation.""" +from collections.abc import Callable + from guppylang.decorator import guppy @@ -47,3 +49,19 @@ def pure_func2(x: int) -> int: return pure_func1(pure_func1(x)) + 1 validate(pure_func2.compile_function()) + + +def test_pure_callable_from_impure(validate): + @guppy + def impure_func(pure_f: Callable[[int], int, []]) -> int: + return pure_f(5) + 1 + + validate(impure_func.compile_function()) + + +def test_pure_callable_from_pure(validate): + @guppy(max_effects=[]) + def pure_func(pure_f: Callable[[int], int, []]) -> int: + return pure_f(5) + 1 + + validate(pure_func.compile_function()) From 8782e3e73f8de1b877ab9fd2be12cb80f5df35a3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 19:27:57 +0100 Subject: [PATCH 21/81] pure_calls_impure_def: remove comment about result, we've done that now --- .../error/effects_errors/pure_calls_impure_def.err | 14 +++++++------- .../error/effects_errors/pure_calls_impure_def.py | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 99395cb73..ffc42de41 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -1,9 +1,9 @@ -Error: Too many effects (at $FILE:10:10) - | - 8 | @guppy(max_effects=[]) - 9 | def main() -> int: -10 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(max_effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Callee of type `int -> int` has effects + | that exceed the allowed effects `[]` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.py b/tests/error/effects_errors/pure_calls_impure_def.py index 97dfc68a0..abaaaeb53 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.py +++ b/tests/error/effects_errors/pure_calls_impure_def.py @@ -2,7 +2,6 @@ @guppy def impure_func(x: int) -> int: - #Use result, or similar? return x + 1 @guppy(max_effects=[]) From 6224053b7cf2aa96843e62ef6889ed40ad9a3d42 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 19:40:51 +0100 Subject: [PATCH 22/81] test no contravariance of argument type --- tests/error/type_errors/fun_ty_mismatch_4.err | 9 +++++++++ tests/error/type_errors/fun_ty_mismatch_4.py | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/error/type_errors/fun_ty_mismatch_4.err create mode 100644 tests/error/type_errors/fun_ty_mismatch_4.py diff --git a/tests/error/type_errors/fun_ty_mismatch_4.err b/tests/error/type_errors/fun_ty_mismatch_4.err new file mode 100644 index 000000000..2bcce2aad --- /dev/null +++ b/tests/error/type_errors/fun_ty_mismatch_4.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:10:11) + | + 8 | return x > 0 + 9 | +10 | return bar + | ^^^ Expected return value of type `nat -> bool`, got `int -> + | bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/fun_ty_mismatch_4.py b/tests/error/type_errors/fun_ty_mismatch_4.py new file mode 100644 index 000000000..70848b00c --- /dev/null +++ b/tests/error/type_errors/fun_ty_mismatch_4.py @@ -0,0 +1,10 @@ +from collections.abc import Callable + +from tests.util import compile_guppy + +@compile_guppy +def foo() -> Callable[[nat], bool]: + def bar(x: int) -> bool: + return x > 0 + + return bar From 4284ee96a2770ede34eb97fc13ddb4ad854e39aa Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 19:41:21 +0100 Subject: [PATCH 23/81] no, test no covariance of return type, simpler right --- tests/error/type_errors/fun_ty_mismatch_4.err | 11 +++++------ tests/error/type_errors/fun_ty_mismatch_4.py | 8 +++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/error/type_errors/fun_ty_mismatch_4.err b/tests/error/type_errors/fun_ty_mismatch_4.err index 2bcce2aad..72b625967 100644 --- a/tests/error/type_errors/fun_ty_mismatch_4.err +++ b/tests/error/type_errors/fun_ty_mismatch_4.err @@ -1,9 +1,8 @@ -Error: Type mismatch (at $FILE:10:11) +Error: Type mismatch (at $FILE:12:11) | - 8 | return x > 0 - 9 | -10 | return bar - | ^^^ Expected return value of type `nat -> bool`, got `int -> - | bool` +10 | return x +11 | +12 | return bar + | ^^^ Expected return value of type `nat -> int`, got `nat -> nat` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/fun_ty_mismatch_4.py b/tests/error/type_errors/fun_ty_mismatch_4.py index 70848b00c..386b2df4b 100644 --- a/tests/error/type_errors/fun_ty_mismatch_4.py +++ b/tests/error/type_errors/fun_ty_mismatch_4.py @@ -2,9 +2,11 @@ from tests.util import compile_guppy + @compile_guppy -def foo() -> Callable[[nat], bool]: - def bar(x: int) -> bool: - return x > 0 +def foo() -> Callable[[nat], int]: + # This has a narrower return type, but we enforce invariance of Callable types, so this is still an error. + def bar(x: nat) -> nat: + return x return bar From 8965c2e4d220ba65e5cbca30bffc171a8c464ef0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 19:29:05 +0100 Subject: [PATCH 24/81] Enforce invariance of Callable max-effects when assignment, +test +printing --- .../src/guppylang_internals/tys/printing.py | 9 +++++++-- .../src/guppylang_internals/tys/ty.py | 6 ++++++ .../error/effects_errors/return_impure_callable.err | 9 +++++++++ tests/error/effects_errors/return_impure_callable.py | 11 +++++++++++ tests/error/effects_errors/return_pure_callable.err | 9 +++++++++ tests/error/effects_errors/return_pure_callable.py | 12 ++++++++++++ 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/error/effects_errors/return_impure_callable.err create mode 100644 tests/error/effects_errors/return_impure_callable.py create mode 100644 tests/error/effects_errors/return_pure_callable.err create mode 100644 tests/error/effects_errors/return_pure_callable.py diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index b92e06031..121c15e38 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -104,8 +104,13 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: ] quantified = ", ".join(params) del self.bound_names[: -len(ty.params)] - return _wrap(f"forall {quantified}. {inputs} -> {output}", inside_row) - return _wrap(f"{inputs} -> {output}", inside_row) + desc = f"forall {quantified}. {inputs} -> {output}" + else: + desc = f"{inputs} -> {output}" + if ty.max_effects is not None: + effects = ", ".join(ty.max_effects) + desc += f" w/fx [{effects}]" + return _wrap(desc, inside_row) @_visit.register(OpaqueType) @_visit.register(StructType) diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index d561d8a15..a7ce34ba0 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -893,6 +893,12 @@ def unify(s: Type | Const, t: Type | Const, subst: "Subst | None") -> "Subst | N case FunctionType() as s, FunctionType() as t if s.params == t.params: if len(s.inputs) != len(t.inputs): return None + if s.max_effects != t.max_effects: + # There are no "effect variables" yet, and we enforce exact matching + # (invariance) as covariance will become difficult when we replace Order + # edges with explicit tokens. (Requiring runtime closures or codegen for + # a statically-predictable function being assigned.) + return None for a, b in zip(s.inputs, t.inputs, strict=True): if a.ty.linear and b.ty.linear and a.flags != b.flags: return None diff --git a/tests/error/effects_errors/return_impure_callable.err b/tests/error/effects_errors/return_impure_callable.err new file mode 100644 index 000000000..24990eaf7 --- /dev/null +++ b/tests/error/effects_errors/return_impure_callable.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:9:10) + | +7 | @guppy +8 | def main() -> Callable[[int], int, []]: +9 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -> int w/fx []`, got `int + | -> int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_impure_callable.py b/tests/error/effects_errors/return_impure_callable.py new file mode 100644 index 000000000..6ed02bd56 --- /dev/null +++ b/tests/error/effects_errors/return_impure_callable.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy + +@guppy +def impure_func(x: int) -> int: + return x + 1 + +@guppy +def main() -> Callable[[int], int, []]: + return impure_func + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/return_pure_callable.err b/tests/error/effects_errors/return_pure_callable.err new file mode 100644 index 000000000..9546f9ad3 --- /dev/null +++ b/tests/error/effects_errors/return_pure_callable.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:10:10) + | + 8 | @guppy + 9 | def main() -> Callable[[int], int]: +10 | return pure_func + | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -> int + | w/fx []` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_pure_callable.py b/tests/error/effects_errors/return_pure_callable.py new file mode 100644 index 000000000..f2678d344 --- /dev/null +++ b/tests/error/effects_errors/return_pure_callable.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy + +@guppy(max_effects=[]) +def pure_func(x: int) -> int: + return x + 1 + +# This is an error because we enforce invariance of Callable types. +@guppy +def main() -> Callable[[int], int]: + return pure_func + +main.compile() \ No newline at end of file From 5cf12c7d371359e00b44bf8a5f8bc7fe01ecb02f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 19:45:14 +0100 Subject: [PATCH 25/81] pretty-print as -[]-> --- .../src/guppylang_internals/tys/printing.py | 8 +++----- tests/error/effects_errors/return_impure_callable.err | 4 ++-- tests/error/effects_errors/return_pure_callable.err | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index 121c15e38..7b72b8cba 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -95,6 +95,7 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: if len(ty.inputs) != 1: inputs = f"({inputs})" output = self._visit(ty.output, True) + arrow = "->" if ty.max_effects is None else f"-[{', '.join(ty.max_effects)}]->" if ty.parametrized: params = [ self._visit(param, False) @@ -104,12 +105,9 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: ] quantified = ", ".join(params) del self.bound_names[: -len(ty.params)] - desc = f"forall {quantified}. {inputs} -> {output}" + desc = f"forall {quantified}. {inputs} {arrow} {output}" else: - desc = f"{inputs} -> {output}" - if ty.max_effects is not None: - effects = ", ".join(ty.max_effects) - desc += f" w/fx [{effects}]" + desc = f"{inputs} {arrow} {output}" return _wrap(desc, inside_row) @_visit.register(OpaqueType) diff --git a/tests/error/effects_errors/return_impure_callable.err b/tests/error/effects_errors/return_impure_callable.err index 24990eaf7..21e2e90e4 100644 --- a/tests/error/effects_errors/return_impure_callable.err +++ b/tests/error/effects_errors/return_impure_callable.err @@ -3,7 +3,7 @@ Error: Type mismatch (at $FILE:9:10) 7 | @guppy 8 | def main() -> Callable[[int], int, []]: 9 | return impure_func - | ^^^^^^^^^^^ Expected return value of type `int -> int w/fx []`, got `int - | -> int` + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> + | int` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_pure_callable.err b/tests/error/effects_errors/return_pure_callable.err index 9546f9ad3..c6cfb353c 100644 --- a/tests/error/effects_errors/return_pure_callable.err +++ b/tests/error/effects_errors/return_pure_callable.err @@ -3,7 +3,7 @@ Error: Type mismatch (at $FILE:10:10) 8 | @guppy 9 | def main() -> Callable[[int], int]: 10 | return pure_func - | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -> int - | w/fx []` + | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -[]-> + | int` Guppy compilation failed due to 1 previous error From ba705c8119a5b9efd1cc50d4ce06e534e195ad46 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 May 2026 21:35:30 +0100 Subject: [PATCH 26/81] list[str](|None) -> list[tys.Effect](|None) --- .../checker/cfg_checker.py | 5 +++-- .../src/guppylang_internals/checker/core.py | 3 ++- .../checker/errors/type_errors.py | 5 +++-- .../checker/modifier_checker.py | 3 ++- .../src/guppylang_internals/decorator.py | 5 +++-- .../guppylang_internals/definition/custom.py | 3 ++- .../definition/declaration.py | 3 ++- .../definition/function.py | 3 ++- .../guppylang_internals/definition/traced.py | 3 ++- .../src/guppylang_internals/tys/__init__.py | 12 ++++++++++ .../src/guppylang_internals/tys/parsing.py | 13 ++++++----- .../src/guppylang_internals/tys/printing.py | 6 ++++- .../src/guppylang_internals/tys/ty.py | 7 +++--- guppylang/src/guppylang/decorator.py | 22 ++++++++++++++++--- 14 files changed, 68 insertions(+), 25 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 06bf13294..b5124bc7d 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -25,6 +25,7 @@ from guppylang_internals.checker.stmt_checker import StmtChecker from guppylang_internals.diagnostic import Error, Note from guppylang_internals.error import GuppyError +from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.ty import InputFlags, Type @@ -76,7 +77,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects: list[str] | None, + max_effects: list[Effect] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -231,7 +232,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects: list[str] | None, + max_effects: list[Effect] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index e9b1a2787..6f7f316a8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -26,6 +26,7 @@ ) from guppylang_internals.engine import BUILTIN_DEFS, DEF_STORE, ENGINE from guppylang_internals.error import InternalGuppyError, RequiresMonomorphizationError +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 ( @@ -447,7 +448,7 @@ class Context(NamedTuple): globals: Globals locals: Locals[str, Variable] generic_param_inst: dict[str, Argument] - max_effects: list[str] | None = None + max_effects: list[Effect] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index c6ee19705..3ff287e89 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from guppylang_internals.definition.util import CheckedField + from guppylang_internals.tys import Effect from guppylang_internals.tys.const import Const from guppylang_internals.tys.param import TypeParam from guppylang_internals.tys.ty import FunctionType, Type @@ -56,8 +57,8 @@ class TooManyEffectsError(Error): "that exceed the allowed effects `{allowed_effects}`" ) ty: Type - effects: list[str] | str - allowed_effects: list[str] + effects: list[Effect] | str + allowed_effects: list[Effect] # ALAN would be good to Note both where the callee is defined # and where the caller is declared as excluding those effects. diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index d00d1aae0..0a7519aa4 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -10,6 +10,7 @@ from guppylang_internals.definition.common import DefId from guppylang_internals.error import GuppyError from guppylang_internals.nodes import CheckedModifiedBlock, ModifiedBlock +from guppylang_internals.tys import Effect from guppylang_internals.tys.ty import ( FuncInput, FunctionType, @@ -23,7 +24,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects: list[str] | None, + max_effects: list[Effect] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index f9d905c2d..f1e1f3980 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -59,6 +59,7 @@ from collections.abc import Callable, Sequence from types import FrameType + from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst @@ -88,7 +89,7 @@ def custom_function( signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, has_var_args: bool = False, - max_effects: list[str] | None = None, + max_effects: list[Effect] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to add custom typing or compilation behaviour to function decls. @@ -128,7 +129,7 @@ def hugr_op( name: str = "", signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, - max_effects: list[str] | None = None, + max_effects: list[Effect] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to annotate function declarations as HUGR ops. diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index ca6551d83..fb772037d 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -39,6 +39,7 @@ make_opaque, read_bool, ) +from guppylang_internals.tys import Effect from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import ( @@ -124,7 +125,7 @@ class RawCustomFunctionDef(ParsableDef): # in Guppy functions in general but some custom functions make use of them). has_var_args: bool = field(default=False) - max_effects: list[str] | None = field(default=None) + max_effects: list[Effect] | None = field(default=None) description: str = field(default="function", init=False) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 39946b486..6b3f9c2ce 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -46,6 +46,7 @@ from guppylang_internals.metadata.common import FunctionMetadata, add_metadata from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap +from guppylang_internals.tys import Effect from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import Type, UnitaryFlags @@ -92,7 +93,7 @@ class RawFunctionDecl(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[str] | None = field(default=None, kw_only=True) + max_effects: list[Effect] | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": """Parses and checks the user-provided signature of the function.""" diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 5229e185a..acc3d54c1 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -53,6 +53,7 @@ from guppylang_internals.metadata.common import FunctionMetadata, add_metadata from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, to_span +from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import ConstArg, TypeArg from guppylang_internals.tys.const import ConstValue from guppylang_internals.tys.subst import Inst, Subst @@ -117,7 +118,7 @@ class RawFunctionDef(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[str] | None = field(default=None, kw_only=True) + max_effects: list[Effect] | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": """Parses and checks the user-provided signature of the function.""" diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index cc3834076..0c267b818 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -39,6 +39,7 @@ from guppylang_internals.metadata.common import FunctionMetadata, add_metadata from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap +from guppylang_internals.tys import Effect from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import FunctionType, Type, UnitaryFlags, type_to_row @@ -55,7 +56,7 @@ class RawTracedFunctionDef(ParsableDef): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[str] | None = field(default=None, kw_only=True) + max_effects: list[Effect] | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": """Parses and checks the user-provided signature of the function.""" diff --git a/guppylang-internals/src/guppylang_internals/tys/__init__.py b/guppylang-internals/src/guppylang_internals/tys/__init__.py index e69de29bb..d5f433fac 100644 --- a/guppylang-internals/src/guppylang_internals/tys/__init__.py +++ b/guppylang-internals/src/guppylang_internals/tys/__init__.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class Effect(Enum): + names = () + + @classmethod + def __from_str__(cls, s: str) -> "Effect": + for effect in cls: + if effect.name == s: + return effect + raise ValueError(f"Invalid effect name: {s}") diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 10ffe3b30..58c21e1d4 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -17,6 +17,7 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError +from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument, ConstArg, TypeArg from guppylang_internals.tys.builtin import CallableTypeDef, SelfTypeDef, bool_type from guppylang_internals.tys.const import ConstValue @@ -247,7 +248,7 @@ def _parse_callable_type( inputs = [parse_function_arg_annotation(inp, None, ctx) for inp in inputs.elts] output = type_from_ast(output, ctx) - max_effects: list[str] | None + max_effects: list[Effect] | None if len(args) == 2: max_effects = None elif not isinstance(args[2], ast.List): @@ -255,12 +256,12 @@ def _parse_callable_type( else: max_effects = [] for e in args[2].elts: - # TODO The max_effects should be a list of `class Effect` i.e. a - # guppylang_internals version of that in guppylang.decorator. - # Then we could parse each to an element of that. - if True or (not isinstance(e, ast.Name)): # noqa: SIM222 + if not isinstance(e, ast.Name): raise GuppyError(err) - max_effects.append(e.id) + try: + max_effects.append(Effect.__from_str__(e.id)) + except ValueError: + raise GuppyError(err) # noqa: B904 return FunctionType(inputs, output, max_effects=max_effects) diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index 7b72b8cba..7c9f821d6 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -95,7 +95,11 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: if len(ty.inputs) != 1: inputs = f"({inputs})" output = self._visit(ty.output, True) - arrow = "->" if ty.max_effects is None else f"-[{', '.join(ty.max_effects)}]->" + arrow = ( + "->" + if ty.max_effects is None + else f"-[{', '.join(str(e) for e in ty.max_effects)}]->" + ) if ty.parametrized: params = [ self._visit(param, False) diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index a7ce34ba0..ed2392cda 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -10,6 +10,7 @@ from hugr import tys as ht from guppylang_internals.error import InternalGuppyError +from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument, ConstArg, TypeArg from guppylang_internals.tys.common import ( ToHugr, @@ -413,7 +414,7 @@ class FunctionType(ParametrizedTypeBase): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, init=True) - max_effects: list[str] | None = field(default=None, init=True) + max_effects: list[Effect] | None = field(default=None, init=True) def __init__( self, @@ -422,7 +423,7 @@ def __init__( params: Sequence[Parameter] | None = None, comptime_args: Sequence[ConstArg] | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, - max_effects: list[str] | None = None, + max_effects: list[Effect] | None = None, ) -> None: # We need a custom __init__ to set the args args: list[Argument] = [TypeArg(inp.ty) for inp in inputs] @@ -605,7 +606,7 @@ def with_unitary_flags(self, flags: UnitaryFlags) -> "FunctionType": max_effects=self.max_effects, ) - def with_effects(self, max_effects: list[str] | None) -> "FunctionType": + def with_effects(self, max_effects: list[Effect] | None) -> "FunctionType": """Returns a copy of this function type with the specified max_effects.""" # N.B. we can't use `dataclasses.replace` here since `FunctionType` has a custom # constructor diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 61fbe6be0..3e2821d86 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -4,7 +4,16 @@ from collections.abc import Callable, Sequence from enum import Enum from types import FrameType -from typing import Any, NamedTuple, ParamSpec, TypedDict, TypeVar, cast, overload +from typing import ( + Any, + NamedTuple, + ParamSpec, + TypedDict, + TypeVar, + assert_never, + cast, + overload, +) from guppylang_internals.ast_util import annotate_location from guppylang_internals.compiler.core import ( @@ -46,6 +55,7 @@ from guppylang_internals.metadata.common import FunctionMetadata from guppylang_internals.span import Loc, SourceMap, Span from guppylang_internals.tracing.util import hide_trace +from guppylang_internals.tys import Effect as _Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst @@ -92,6 +102,12 @@ class Effect(Enum): # No instances yet. names = () + def to_internal(self) -> _Effect: + match self: + case _ as unreachable: + # Seems assert_never doesn't handle Enum's without cases yet + assert_never(unreachable) # type: ignore[arg-type] + class GuppyKwargs(TypedDict, total=False): """Typed dictionary specifying the optional keyword arguments for the `@guppy` @@ -772,7 +788,7 @@ class ParsedGuppyKwargs(NamedTuple): metadata: FunctionMetadata # The empty list means no effects, whereas None means unspecified - i.e. assume all # effects are possible until we can analyse the call-graph to calculate exactly. - max_effects: list[str] | None + max_effects: list[_Effect] | None link_name: str | None @@ -800,7 +816,7 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: max_effects = ( None if max_effects_input is None - else [effect._name_ for effect in max_effects_input] + else [effect.to_internal() for effect in max_effects_input] ) if remaining := next(iter(kwargs), None): From 797c20b55b667c62d9363ec955be053328c48c18 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 17 May 2026 21:10:22 +0100 Subject: [PATCH 27/81] Show where caller's effects are declared (not callee) --- .../checker/cfg_checker.py | 12 ++++---- .../src/guppylang_internals/checker/core.py | 2 +- .../checker/errors/type_errors.py | 10 +++---- .../checker/expr_checker.py | 13 +++++--- .../checker/func_checker.py | 30 ++++++++++++++++--- .../checker/modifier_checker.py | 6 ++-- .../checker/stmt_checker.py | 2 +- .../pure_calls_impure_callable.err | 8 ++++- .../effects_errors/pure_calls_impure_decl.err | 8 ++++- .../effects_errors/pure_calls_impure_def.err | 8 ++++- 10 files changed, 72 insertions(+), 27 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index b5124bc7d..69f3a0552 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, field from typing import ClassVar, Generic, TypeVar -from guppylang_internals.ast_util import line_col +from guppylang_internals.ast_util import AstNode, line_col from guppylang_internals.cfg.bb import BB from guppylang_internals.cfg.cfg import CFG, BaseCFG from guppylang_internals.checker.core import ( @@ -77,7 +77,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects: list[Effect] | None, + max_effects_from: tuple[list[Effect], AstNode] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -107,7 +107,7 @@ def check_cfg( return_ty, generic_args, globals, - max_effects=max_effects, + max_effects_from=max_effects_from, ) compiled = {cfg.entry_bb: checked_cfg.entry_bb} @@ -140,7 +140,7 @@ def check_cfg( return_ty, generic_args, globals, - max_effects=max_effects, + max_effects_from=max_effects_from, ) queue += [ # We enumerate the successor starting from the back, so we start with @@ -232,7 +232,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects: list[Effect] | None, + max_effects_from: tuple[list[Effect], AstNode] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg @@ -250,7 +250,7 @@ def check_bb( # Check the basic block ctx = Context( - globals, Locals({v.name: v for v in inputs}), generic_args, max_effects + 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) diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index 6f7f316a8..c9c964ae5 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -448,7 +448,7 @@ class Context(NamedTuple): globals: Globals locals: Locals[str, Variable] generic_param_inst: dict[str, Argument] - max_effects: list[Effect] | None = None + max_effects_from: tuple[list[Effect], AstNode] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 3ff287e89..bd60d2805 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -53,15 +53,15 @@ class ConstMismatchError(Error): class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects {effects}\n" - "that exceed the allowed effects `{allowed_effects}`" + "Callee of type `{ty}` has effects {effects}\nthat exceed the allowed effects" ) ty: Type effects: list[Effect] | str - allowed_effects: list[Effect] - # ALAN would be good to Note both where the callee is defined - # and where the caller is declared as excluding those effects. + @dataclass(frozen=True) + class MaxFromDecl(Note): + span_label: ClassVar[str] = "Allowed effects `{allowed_effects}` declared here" + allowed_effects: list[Effect] @dataclass(frozen=True) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 6af0e0ac4..0afb3680f 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1254,14 +1254,17 @@ def check_comptime_arg( 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 ctx.max_effects is not None and ( + if ctx.max_effects_from is not None and ( func_ty.max_effects is None - or any(e not in ctx.max_effects for e in func_ty.max_effects) + or any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) ): loc_node = node.func if isinstance(node, ast.Call) else node effects = "" if func_ty.max_effects is None else func_ty.max_effects + effects_allowed, effects_decl = ctx.max_effects_from raise GuppyTypeError( - TooManyEffectsError(loc_node, func_ty, effects, ctx.max_effects) + TooManyEffectsError(loc_node, func_ty, effects).add_sub_diagnostic( + TooManyEffectsError.MaxFromDecl(effects_decl, effects_allowed) + ) ) @@ -1502,7 +1505,9 @@ def check_generator( ctx.globals, inner_locals, ctx.generic_param_inst, - ctx.max_effects, + # If the nested func *could* change the max_effects then we'd use its + # declaration here, but we don't allow that. + ctx.max_effects_from, ) expr_sth, stmt_chk = ExprSynthesizer(inner_ctx), StmtChecker(inner_ctx) gen.iter, iter_ty = expr_sth.visit(gen.iter) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 22b6f1df7..47c691e9c 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -22,7 +22,7 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.diagnostic import Error, Help, Note from guppylang_internals.engine import DEF_STORE, ENGINE -from guppylang_internals.error import GuppyError +from guppylang_internals.error import GuppyError, InternalGuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef from guppylang_internals.tys.parsing import ( @@ -156,6 +156,15 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } + if ty.max_effects is None: + max_effects_from = None + else: + dec = _find_guppy_decorator(func_def.decorator_list) + if dec is None: + raise InternalGuppyError( + "Expected to find a `@guppy` decorator on a function with max effects" + ) + max_effects_from = (ty.max_effects, dec) return check_cfg( cfg, inputs, @@ -163,10 +172,21 @@ def check_global_func_def( generic_args, func_def.name, globals, - max_effects=ty.max_effects, + max_effects_from=max_effects_from, ) +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" # or declare? + ): + return d + return None + + def check_nested_func_def( func_def: NestedFunctionDef, bb: BB, @@ -177,7 +197,9 @@ def check_nested_func_def( # 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. - func_ty = check_signature(func_def, ctx.globals).with_effects(ctx.max_effects) + func_ty = check_signature(func_def, ctx.globals).with_effects( + None if ctx.max_effects_from is None else ctx.max_effects_from[0] + ) assert func_ty.input_names is not None if func_ty.parametrized: @@ -263,7 +285,7 @@ def check_nested_func_def( {}, func_def.name, globals, - max_effects=func_ty.max_effects, + max_effects_from=ctx.max_effects_from, ) checked_def = CheckedNestedFunctionDef( def_id, diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index 0a7519aa4..e43abf734 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -2,7 +2,7 @@ import ast -from guppylang_internals.ast_util import loop_in_ast, with_loc +from guppylang_internals.ast_util import AstNode, loop_in_ast, 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 @@ -24,7 +24,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects: list[Effect] | None, + max_effects_from: tuple[list[Effect], AstNode] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg @@ -77,7 +77,7 @@ def check_modified_block( {}, "__modified__()", globals, - max_effects=max_effects, + 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, ) diff --git a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py index 447deb010..f2dc4bab8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/stmt_checker.py @@ -421,7 +421,7 @@ def visit_ModifiedBlock(self, node: ModifiedBlock) -> ast.stmt: # check the body of the modified block modified_block = check_modified_block( - node, self.bb, self.ctx, max_effects=self.ctx.max_effects + node, self.bb, self.ctx, max_effects_from=self.ctx.max_effects_from ) # check the arguments of the control and power. diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 7e958b1a4..0e69c3eb3 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -4,6 +4,12 @@ Error: Too many effects (at $FILE:5:10) 4 | def main(impure_f: Callable[[int], int]) -> int: 5 | return impure_f(5) | ^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | that exceed the allowed effects + +Note: + | +2 | +3 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 5a016f346..32d59ce47 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -4,6 +4,12 @@ Error: Too many effects (at $FILE:8:10) 7 | def main() -> int: 8 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | that exceed the allowed effects + +Note: + | +5 | +6 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index ffc42de41..6d941de6e 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -4,6 +4,12 @@ Error: Too many effects (at $FILE:9:10) 8 | def main() -> int: 9 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects `[]` + | that exceed the allowed effects + +Note: + | +6 | +7 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error From ab10061a5d0eab589eae5f331dde727c92905134 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 17 May 2026 21:17:52 +0100 Subject: [PATCH 28/81] move comment --- .../src/guppylang_internals/checker/expr_checker.py | 2 -- .../src/guppylang_internals/checker/func_checker.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 0afb3680f..1bdabe35b 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1505,8 +1505,6 @@ def check_generator( ctx.globals, inner_locals, ctx.generic_param_inst, - # If the nested func *could* change the max_effects then we'd use its - # declaration here, but we don't allow that. ctx.max_effects_from, ) expr_sth, stmt_chk = ExprSynthesizer(inner_ctx), StmtChecker(inner_ctx) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 47c691e9c..7729a97e7 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -285,6 +285,8 @@ def check_nested_func_def( {}, 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( From 39c6c8c2d8fbdf25de109ae252fdf2864388b80f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 18 May 2026 11:45:32 +0100 Subject: [PATCH 29/81] Further refine error message, use wrap --- .../src/guppylang_internals/checker/errors/type_errors.py | 2 +- tests/error/effects_errors/pure_calls_impure_callable.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_decl.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_def.err | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index bd60d2805..fa2b2acc0 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -53,7 +53,7 @@ class ConstMismatchError(Error): class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects {effects}\nthat exceed the allowed effects" + "Callee of type `{ty}` has effects {effects} that exceed those allowed" ) ty: Type effects: list[Effect] | str diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 0e69c3eb3..86008d3eb 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:5:10) 3 | @guppy(max_effects=[]) 4 | def main(impure_f: Callable[[int], int]) -> int: 5 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects + | ^^^^^^^^ Callee of type `int -> int` has effects that + | exceed those allowed Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 32d59ce47..3e0ba947e 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(max_effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects + | ^^^^^^^^^^^ Callee of type `int -> int` has effects that + | exceed those allowed Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 6d941de6e..a60558830 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(max_effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects - | that exceed the allowed effects + | ^^^^^^^^^^^ Callee of type `int -> int` has effects that + | exceed those allowed Note: | From a307f0274970ead6f6628c1298456477da031403 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 18 May 2026 11:54:21 +0100 Subject: [PATCH 30/81] overloading: check effects last, pass on error via isinstance --- .../checker/expr_checker.py | 10 ++++++---- .../definition/overloaded.py | 18 ++++++++++++++--- tests/error/effects_errors/pure_result.err | 20 ++++++++----------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 1bdabe35b..1dcb7d9e8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1279,8 +1279,6 @@ def synthesize_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(args), node, func_ty) - _check_effects(func_ty, ctx, node) - # Replace quantified variables with free unification variables and try to infer an # instantiation by checking the arguments unquantified, free_vars = func_ty.unquantified() @@ -1293,6 +1291,9 @@ def synthesize_call( # Finally, check that the instantiation respects the linearity requirements check_inst(func_ty, inst, node) + # Check effects last so we can avoid using them to resolve overloading + _check_effects(func_ty, ctx, node) + return args, unquantified.output.substitute(subst), inst @@ -1312,8 +1313,6 @@ def check_call( assert not func_ty.unsolved_vars check_num_args(len(func_ty.inputs), len(inputs), node, func_ty) - _check_effects(func_ty, ctx, node) - # When checking, we can use the information from the expected return type to infer # some type arguments. However, this pushes errors inwards. For example, given a # function `foo: forall T. T -> T`, the following type mismatch would be reported: @@ -1381,6 +1380,9 @@ def check_call( # Finally, check that the instantiation respects the linearity requirements check_inst(func_ty, inst, node) + # Check effects last so we can avoid using them to resolve overloading + _check_effects(func_ty, ctx, node) + return inputs, subst, inst diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index f46dbd584..f1c349de3 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -1,6 +1,5 @@ import ast import copy -from contextlib import suppress from dataclasses import dataclass, field from typing import ClassVar, NamedTuple, NoReturn @@ -8,6 +7,7 @@ from guppylang_internals.ast_util import AstNode from guppylang_internals.checker.core import Context +from guppylang_internals.checker.errors.type_errors import TooManyEffectsError from guppylang_internals.checker.expr_checker import ExprSynthesizer from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.definition.common import ( @@ -102,12 +102,18 @@ def check_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - with suppress(GuppyError): + try: # check_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) args_copy = copy.deepcopy(args) return defn.check_call(args_copy, ty, node_copy, ctx) + except GuppyError as e: + if isinstance(e.error, TooManyEffectsError): + # We do not allow overloading on effects, so if this error is raised + # then this is the relevant overload, so report the error. + raise + continue return self._call_error(args, node, ctx, available_sigs, ty) def synthesize_call( @@ -119,12 +125,18 @@ def synthesize_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - with suppress(GuppyError): + try: # synthesize_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) args_copy = copy.deepcopy(args) return defn.synthesize_call(args_copy, node_copy, ctx) + except GuppyError as e: + if isinstance(e.error, TooManyEffectsError): + # We do not allow overloading on effects, so if this error is raised + # then this is the relevant overload, so report the error. + raise + continue return self._call_error(args, node, ctx, available_sigs) def _call_error( diff --git a/tests/error/effects_errors/pure_result.err b/tests/error/effects_errors/pure_result.err index ec2dbcd14..5aee34546 100644 --- a/tests/error/effects_errors/pure_result.err +++ b/tests/error/effects_errors/pure_result.err @@ -1,19 +1,15 @@ -Error: Invalid call of overloaded function (at $FILE:6:10) +Error: Too many effects (at $FILE:6:3) | 4 | @guppy(max_effects=[]) 5 | def main() -> int: 6 | result("foo", True) - | ^^^^^^^^^^^ No variant of overloaded function `result` takes arguments - | `str`, `bool` + | ^^^^^^ Callee of type `forall . (str @comptime, bool) -> None` has + | effects that exceed those allowed -Note: Available overloads are: - def result(tag: str @comptime, value: int) -> None - def result(tag: str @comptime, value: nat) -> None - def result(tag: str @comptime, value: bool) -> None - def result(tag: str @comptime, value: float) -> None - def result(tag: str @comptime, value: array[int, n]) -> None - def result(tag: str @comptime, value: array[nat, n]) -> None - def result(tag: str @comptime, value: array[bool, n]) -> None - def result(tag: str @comptime, value: array[float, n]) -> None +Note: + | +3 | +4 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error From 028b8d554649d44edb41a9a0f8df18e3e3177fdd Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 20 May 2026 12:23:17 +0100 Subject: [PATCH 31/81] overloaded.py: whitelist errors to ignore rather than suppressing all --- .../definition/overloaded.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index f1c349de3..febc822d5 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -7,7 +7,11 @@ from guppylang_internals.ast_util import AstNode from guppylang_internals.checker.core import Context -from guppylang_internals.checker.errors.type_errors import TooManyEffectsError +from guppylang_internals.checker.errors.comptime_errors import ComptimeUnknownError +from guppylang_internals.checker.errors.type_errors import ( + TypeMismatchError, + WrongNumberOfArgsError, +) from guppylang_internals.checker.expr_checker import ExprSynthesizer from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.definition.common import ( @@ -109,11 +113,21 @@ def check_call( args_copy = copy.deepcopy(args) return defn.check_call(args_copy, ty, node_copy, ctx) except GuppyError as e: - if isinstance(e.error, TooManyEffectsError): - # We do not allow overloading on effects, so if this error is raised - # then this is the relevant overload, so report the error. - raise - continue + if isinstance( + e.error, + ( + TypeMismatchError, + WrongNumberOfArgsError, + OverloadNoMatchError, + ComptimeUnknownError, + InternalExpectOverloadError, + ), + ): + continue # Try the next overload + # Pass on e.g. TooManyEffectsError since we do not allow overloading + # on effects, and effects are checked only after other arguments that + # ensure this is the correct overload. + raise return self._call_error(args, node, ctx, available_sigs, ty) def synthesize_call( @@ -132,11 +146,21 @@ def synthesize_call( args_copy = copy.deepcopy(args) return defn.synthesize_call(args_copy, node_copy, ctx) except GuppyError as e: - if isinstance(e.error, TooManyEffectsError): - # We do not allow overloading on effects, so if this error is raised - # then this is the relevant overload, so report the error. - raise - continue + if isinstance( + e.error, + ( + TypeMismatchError, + WrongNumberOfArgsError, + OverloadNoMatchError, + ComptimeUnknownError, + InternalExpectOverloadError, + ), + ): + continue # Try the next overload + # Pass on e.g. TooManyEffectsError since we do not allow overloading + # on effects, and effects are checked only after other arguments that + # ensure this is the correct overload. + raise return self._call_error(args, node, ctx, available_sigs) def _call_error( From 4e97214b30ab60675ff9d935a32f40c0dc05bbaf Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 20 May 2026 12:33:31 +0100 Subject: [PATCH 32/81] Add local/specific suppress_overload_match_errors contextmanager with whitelist --- .../definition/overloaded.py | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index febc822d5..e23274e1f 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -1,5 +1,7 @@ import ast import copy +from collections.abc import Iterator +from contextlib import contextmanager from dataclasses import dataclass, field from typing import ClassVar, NamedTuple, NoReturn @@ -89,6 +91,28 @@ class InternalExpectOverloadError(Error): ) +@contextmanager +def suppress_overload_match_errors() -> Iterator[None]: + try: + yield + except GuppyError as e: + if isinstance( + e.error, + ( + TypeMismatchError, + WrongNumberOfArgsError, + OverloadNoMatchError, # As OverloadedFunctionDef's can be nested + ComptimeUnknownError, + InternalExpectOverloadError, + ), + ): + return # Try the next overload + # Pass on e.g. TooManyEffectsError since we do not allow overloading on effects + # and effects are checked only after other arguments that ensure this is the + # correct overload. + raise + + @dataclass(frozen=True) class OverloadedFunctionDef(CompiledCallableDef, CallableDef): func_ids: list[DefId] @@ -106,28 +130,12 @@ def check_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - try: + with suppress_overload_match_errors(): # check_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) args_copy = copy.deepcopy(args) return defn.check_call(args_copy, ty, node_copy, ctx) - except GuppyError as e: - if isinstance( - e.error, - ( - TypeMismatchError, - WrongNumberOfArgsError, - OverloadNoMatchError, - ComptimeUnknownError, - InternalExpectOverloadError, - ), - ): - continue # Try the next overload - # Pass on e.g. TooManyEffectsError since we do not allow overloading - # on effects, and effects are checked only after other arguments that - # ensure this is the correct overload. - raise return self._call_error(args, node, ctx, available_sigs, ty) def synthesize_call( @@ -139,28 +147,12 @@ def synthesize_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - try: + with suppress_overload_match_errors(): # synthesize_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) args_copy = copy.deepcopy(args) return defn.synthesize_call(args_copy, node_copy, ctx) - except GuppyError as e: - if isinstance( - e.error, - ( - TypeMismatchError, - WrongNumberOfArgsError, - OverloadNoMatchError, - ComptimeUnknownError, - InternalExpectOverloadError, - ), - ): - continue # Try the next overload - # Pass on e.g. TooManyEffectsError since we do not allow overloading - # on effects, and effects are checked only after other arguments that - # ensure this is the correct overload. - raise return self._call_error(args, node, ctx, available_sigs) def _call_error( From c6c8d4726e8d32272c33e2c79dbddc8049f3f5be Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 13:34:50 +0100 Subject: [PATCH 33/81] allow legality of effects to determine overloading, TODO error hint --- .../definition/overloaded.py | 45 ++++++------------- .../src/guppylang_internals/tys/printing.py | 1 + tests/error/effects_errors/overload.err | 14 ++++++ tests/error/effects_errors/overload.py | 32 +++++++++++++ tests/error/effects_errors/pure_result.err | 20 +++++---- 5 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 tests/error/effects_errors/overload.err create mode 100644 tests/error/effects_errors/overload.py diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index e23274e1f..dec1e3600 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -1,7 +1,6 @@ import ast import copy -from collections.abc import Iterator -from contextlib import contextmanager +from contextlib import suppress from dataclasses import dataclass, field from typing import ClassVar, NamedTuple, NoReturn @@ -9,11 +8,6 @@ from guppylang_internals.ast_util import AstNode from guppylang_internals.checker.core import Context -from guppylang_internals.checker.errors.comptime_errors import ComptimeUnknownError -from guppylang_internals.checker.errors.type_errors import ( - TypeMismatchError, - WrongNumberOfArgsError, -) from guppylang_internals.checker.expr_checker import ExprSynthesizer from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.definition.common import ( @@ -28,6 +22,7 @@ from guppylang_internals.diagnostic import Error, Note from guppylang_internals.error import GuppyError, InternalGuppyError from guppylang_internals.span import Span, to_span +from guppylang_internals.tys import Effect from guppylang_internals.tys.printing import signature_to_str from guppylang_internals.tys.subst import Subst from guppylang_internals.tys.ty import FunctionType, Type @@ -44,6 +39,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None + max_effects_from: tuple[list[Effect], AstNode] | None @property def rendered_span_label(self) -> str: @@ -58,6 +54,11 @@ def rendered_span_label(self) -> str: stem += f"takes arguments {args}" if self.return_ty: stem += f" and returns `{self.return_ty}`" + if self.max_effects_from: + effects, _node = self.max_effects_from + stem += f" with effects no more than {effects}" + else: + stem += " with any effects" return stem @@ -91,28 +92,6 @@ class InternalExpectOverloadError(Error): ) -@contextmanager -def suppress_overload_match_errors() -> Iterator[None]: - try: - yield - except GuppyError as e: - if isinstance( - e.error, - ( - TypeMismatchError, - WrongNumberOfArgsError, - OverloadNoMatchError, # As OverloadedFunctionDef's can be nested - ComptimeUnknownError, - InternalExpectOverloadError, - ), - ): - return # Try the next overload - # Pass on e.g. TooManyEffectsError since we do not allow overloading on effects - # and effects are checked only after other arguments that ensure this is the - # correct overload. - raise - - @dataclass(frozen=True) class OverloadedFunctionDef(CompiledCallableDef, CallableDef): func_ids: list[DefId] @@ -130,7 +109,7 @@ def check_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - with suppress_overload_match_errors(): + with suppress(GuppyError): # check_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) @@ -147,7 +126,7 @@ def synthesize_call( assert isinstance(defn, CallableDef) has_var_args = isinstance(defn, CustomFunctionDef) and defn.has_var_args available_sigs.append(OverloadVariant(defn.ty, has_var_args)) - with suppress_overload_match_errors(): + with suppress(GuppyError): # synthesize_call may modify args and node, # thus we deepcopy them before passing in the function node_copy = copy.deepcopy(node) @@ -172,7 +151,9 @@ def _call_error( synth = ExprSynthesizer(ctx) arg_tys = [synth.synthesize(arg)[1] for arg in args] - err = OverloadNoMatchError(span, self.name, arg_tys, return_ty) + err = OverloadNoMatchError( + span, self.name, arg_tys, return_ty, ctx.max_effects_from + ) err.add_sub_diagnostic(AvailableOverloadsHint(None, self.name, available_sigs)) raise GuppyError(err) diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index 7c9f821d6..9d8a8c44e 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -175,4 +175,5 @@ def signature_to_str(name: str, sig: FunctionType, has_var_args: bool = False) - 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) return s + ") -> " + str(sig.output) diff --git a/tests/error/effects_errors/overload.err b/tests/error/effects_errors/overload.err new file mode 100644 index 000000000..425e1631d --- /dev/null +++ b/tests/error/effects_errors/overload.err @@ -0,0 +1,14 @@ +Error: Invalid call of overloaded function (at $FILE:24:11) + | +22 | @guppy(max_effects=[]) +23 | def bad_pure_func(x: float) -> float: +24 | return only_pure_for_int(x) + | ^^^^^^^^^^^^^^^^^^^^ No variant of overloaded function `only_pure_for_int` takes + | a `float` argument and returns `float` with effects no more + | than [] + +Note: Available overloads are: + def only_pure_for_int(x: T) -> T + def only_pure_for_int(x: int) -> int + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/overload.py b/tests/error/effects_errors/overload.py new file mode 100644 index 000000000..6fbe03729 --- /dev/null +++ b/tests/error/effects_errors/overload.py @@ -0,0 +1,32 @@ +from guppylang.decorator import guppy + +T = guppy.type_var("T") + +@guppy.declare +def variant1(x : T) -> T: ... + +@guppy.declare(max_effects=[]) +def variant2(x : int) -> int: ... + +@guppy.overload(variant1, variant2) +def only_pure_for_int(): ... + +@guppy(max_effects=[]) +def pure_func(x: int) -> int: + return only_pure_for_int(x + 1) + +@guppy +def impure_func(x: float) -> float: + return only_pure_for_int(x + 1.0) + +@guppy(max_effects=[]) +def bad_pure_func(x: float) -> float: + return only_pure_for_int(x) + +@guppy +def main() -> None: + pure_func(5) + impure_func(5.0) + bad_pure_func(3.14) + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/pure_result.err b/tests/error/effects_errors/pure_result.err index 5aee34546..baec08100 100644 --- a/tests/error/effects_errors/pure_result.err +++ b/tests/error/effects_errors/pure_result.err @@ -1,15 +1,19 @@ -Error: Too many effects (at $FILE:6:3) +Error: Invalid call of overloaded function (at $FILE:6:10) | 4 | @guppy(max_effects=[]) 5 | def main() -> int: 6 | result("foo", True) - | ^^^^^^ Callee of type `forall . (str @comptime, bool) -> None` has - | effects that exceed those allowed + | ^^^^^^^^^^^ No variant of overloaded function `result` takes arguments + | `str`, `bool` with effects no more than [] -Note: - | -3 | -4 | @guppy(max_effects=[]) - | --------------------- Allowed effects `[]` declared here +Note: Available overloads are: + def result(tag: str @comptime, value: int) -> None + def result(tag: str @comptime, value: nat) -> None + def result(tag: str @comptime, value: bool) -> None + def result(tag: str @comptime, value: float) -> None + def result(tag: str @comptime, value: array[int, n]) -> None + def result(tag: str @comptime, value: array[nat, n]) -> None + def result(tag: str @comptime, value: array[bool, n]) -> None + def result(tag: str @comptime, value: array[float, n]) -> None Guppy compilation failed due to 1 previous error From 93535ddbda27eb9c3d8556183acf4c303882b07d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 14:40:32 +0100 Subject: [PATCH 34/81] Drop the 'with any effects' in overloaded --- .../src/guppylang_internals/definition/overloaded.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index dec1e3600..625f432f9 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -57,8 +57,6 @@ def rendered_span_label(self) -> str: if self.max_effects_from: effects, _node = self.max_effects_from stem += f" with effects no more than {effects}" - else: - stem += " with any effects" return stem From 5a8053e7aa88a084beb8893e4b9c97696b0fb4c4 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 19:09:00 +0100 Subject: [PATCH 35/81] Add internal Effect.ANY; improve formatting; FunctionType.max_effects(<-_declared) --- .../checker/errors/type_errors.py | 18 +++++++++--- .../checker/expr_checker.py | 8 ++--- .../checker/func_checker.py | 14 +++++---- .../guppylang_internals/definition/custom.py | 2 +- .../definition/overloaded.py | 3 +- .../guppylang_internals/tracing/function.py | 1 + .../src/guppylang_internals/tys/__init__.py | 7 ++++- .../src/guppylang_internals/tys/parsing.py | 2 +- .../src/guppylang_internals/tys/printing.py | 5 ++-- .../src/guppylang_internals/tys/ty.py | 29 +++++++++++++------ .../pure_calls_impure_callable.err | 4 +-- .../effects_errors/pure_calls_impure_decl.err | 4 +-- .../effects_errors/pure_calls_impure_def.err | 4 +-- 13 files changed, 67 insertions(+), 34 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index fa2b2acc0..9caff3f5b 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -4,10 +4,10 @@ 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 - from guppylang_internals.tys import Effect from guppylang_internals.tys.const import Const from guppylang_internals.tys.param import TypeParam from guppylang_internals.tys.ty import FunctionType, Type @@ -53,16 +53,26 @@ class ConstMismatchError(Error): class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects {effects} that exceed those allowed" + "Callee of type `{ty}` has effects {effects_str} that exceed those allowed" ) ty: Type - effects: list[Effect] | str + effects: list[Effect] + + @property + def effects_str(self) -> str: + return Effect.format_list(self.effects) @dataclass(frozen=True) class MaxFromDecl(Note): - span_label: ClassVar[str] = "Allowed effects `{allowed_effects}` declared here" + span_label: ClassVar[str] = ( + "Allowed effects `{allowed_effects_str}` declared here" + ) allowed_effects: list[Effect] + @property + def allowed_effects_str(self) -> str: + return Effect.format_list(self.allowed_effects) + @dataclass(frozen=True) class AssignFieldTypeMismatchError(Error): diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 1dcb7d9e8..8a2321a88 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1255,14 +1255,14 @@ 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 ctx.max_effects_from is not None and ( - func_ty.max_effects is None - or any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) + any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) ): loc_node = node.func if isinstance(node, ast.Call) else node - effects = "" if func_ty.max_effects is None else func_ty.max_effects effects_allowed, effects_decl = ctx.max_effects_from raise GuppyTypeError( - TooManyEffectsError(loc_node, func_ty, effects).add_sub_diagnostic( + TooManyEffectsError( + loc_node, func_ty, func_ty.max_effects + ).add_sub_diagnostic( TooManyEffectsError.MaxFromDecl(effects_decl, effects_allowed) ) ) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 7729a97e7..38c69ba14 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -25,6 +25,7 @@ from guppylang_internals.error import GuppyError, InternalGuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef +from guppylang_internals.tys import Effect from guppylang_internals.tys.parsing import ( TypeParsingCtx, check_function_arg, @@ -156,15 +157,16 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } - if ty.max_effects is None: + if ty.max_effects_declared is None: max_effects_from = None else: dec = _find_guppy_decorator(func_def.decorator_list) - if dec is None: + if dec is None and ty.max_effects_declared is not None: raise InternalGuppyError( - "Expected to find a `@guppy` decorator on a function with max effects" + f"Effects limited to {Effect.format_list(ty.max_effects_declared)}" + " but cannot identify decorator imposing this limit" ) - max_effects_from = (ty.max_effects, dec) + max_effects_from = (ty.max_effects_declared, dec) return check_cfg( cfg, inputs, @@ -181,9 +183,11 @@ def _find_guppy_decorator(decorators: list[ast.expr]) -> ast.expr | None: if ( isinstance(d, ast.Call) and isinstance(d.func, ast.Name) - and d.func.id == "guppy" # or declare? + and d.func.id == "guppy" ): return d + if isinstance(d, ast.Name) and d.id == "guppy": + return d return None diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index fb772037d..a6f78d782 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -125,7 +125,7 @@ class RawCustomFunctionDef(ParsableDef): # in Guppy functions in general but some custom functions make use of them). has_var_args: bool = field(default=False) - max_effects: list[Effect] | None = field(default=None) + max_effects: list[Effect] | None = field(default=None, kw_only=True) description: str = field(default="function", init=False) diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index 625f432f9..ee4d46569 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -56,7 +56,8 @@ def rendered_span_label(self) -> str: stem += f" and returns `{self.return_ty}`" if self.max_effects_from: effects, _node = self.max_effects_from - stem += f" with effects no more than {effects}" + if Effect.ANY not in effects: + stem += f" with effects no more than {effects}" return stem diff --git a/guppylang-internals/src/guppylang_internals/tracing/function.py b/guppylang-internals/src/guppylang_internals/tracing/function.py index 37f4b4eaa..22a46d7c8 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/function.py +++ b/guppylang-internals/src/guppylang_internals/tracing/function.py @@ -182,6 +182,7 @@ def trace_call(func: CallableDef, *args: Any) -> Any: arg_exprs: list[ast.expr] = [ with_loc(state.node, with_type(var.ty, PlaceNode(var))) for var in arg_vars ] + # ALAN add max_effects to Tracing? ctx = Context(Globals(DEF_STORE.frames[func.id]), locals, {}) call_node, ret_ty = func.synthesize_call(arg_exprs, state.node, ctx) diff --git a/guppylang-internals/src/guppylang_internals/tys/__init__.py b/guppylang-internals/src/guppylang_internals/tys/__init__.py index d5f433fac..37ae36a26 100644 --- a/guppylang-internals/src/guppylang_internals/tys/__init__.py +++ b/guppylang-internals/src/guppylang_internals/tys/__init__.py @@ -1,8 +1,9 @@ +from collections.abc import Iterable from enum import Enum class Effect(Enum): - names = () + ANY = "Any" @classmethod def __from_str__(cls, s: str) -> "Effect": @@ -10,3 +11,7 @@ def __from_str__(cls, s: str) -> "Effect": if effect.name == s: return effect raise ValueError(f"Invalid effect name: {s}") + + @staticmethod + def format_list(effects: Iterable["Effect"]) -> str: + return f"[{', '.join(e.name for e in effects)}]" diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 58c21e1d4..33ee006c9 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -262,7 +262,7 @@ def _parse_callable_type( max_effects.append(Effect.__from_str__(e.id)) except ValueError: raise GuppyError(err) # noqa: B904 - return FunctionType(inputs, output, max_effects=max_effects) + return FunctionType(inputs, output, max_effects_declared=max_effects) def _parse_self_type(args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx) -> Type: diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index 9d8a8c44e..68377c186 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -1,6 +1,7 @@ from functools import singledispatchmethod from guppylang_internals.error import InternalGuppyError +from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument, ConstArg, TypeArg from guppylang_internals.tys.const import Const, ConstValue from guppylang_internals.tys.param import ConstParam, TypeParam @@ -97,8 +98,8 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: output = self._visit(ty.output, True) arrow = ( "->" - if ty.max_effects is None - else f"-[{', '.join(str(e) for e in ty.max_effects)}]->" + if ty.max_effects_declared is None + else f"-{Effect.format_list(ty.max_effects_declared)}->" ) if ty.parametrized: params = [ diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index ed2392cda..e546a52bb 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -414,7 +414,18 @@ class FunctionType(ParametrizedTypeBase): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, init=True) - max_effects: list[Effect] | None = field(default=None, init=True) + # The None here is to distinguish between explicit and implicit in guppy source code + # but is otherwise equivalent to default [Effect.ANY]. Generally use + # `max_effects` instead. + max_effects_declared: list[Effect] | None = field(default=None, init=True) + + @property + def max_effects(self) -> list[Effect]: + return ( + self.max_effects_declared + if self.max_effects_declared is not None + else [Effect.ANY] + ) def __init__( self, @@ -423,7 +434,7 @@ def __init__( params: Sequence[Parameter] | None = None, comptime_args: Sequence[ConstArg] | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, - max_effects: list[Effect] | None = None, + max_effects_declared: list[Effect] | None = None, ) -> None: # We need a custom __init__ to set the args args: list[Argument] = [TypeArg(inp.ty) for inp in inputs] @@ -452,7 +463,7 @@ def __init__( object.__setattr__(self, "output", output) object.__setattr__(self, "params", params) object.__setattr__(self, "unitary_flags", unitary_flags) - object.__setattr__(self, "max_effects", max_effects) + object.__setattr__(self, "max_effects_declared", max_effects_declared) @property def parametrized(self) -> bool: @@ -542,7 +553,7 @@ def transform(self, transformer: Transformer) -> "Type": self.params, comptime_args=self.comptime_args, unitary_flags=self.unitary_flags, - max_effects=self.max_effects, + max_effects_declared=self.max_effects_declared, ) def instantiate_partial(self, args: "PartialInst") -> "FunctionType": @@ -572,7 +583,7 @@ def instantiate_partial(self, args: "PartialInst") -> "FunctionType": cast("ConstArg", arg.transform(inst)) for arg in self.comptime_args ], unitary_flags=self.unitary_flags, - max_effects=self.max_effects, + max_effects_declared=self.max_effects_declared, ) def instantiate(self, args: "Inst") -> "FunctionType": @@ -603,14 +614,14 @@ def with_unitary_flags(self, flags: UnitaryFlags) -> "FunctionType": self.params, self.comptime_args, flags, - max_effects=self.max_effects, + max_effects_declared=self.max_effects_declared, ) - def with_effects(self, max_effects: list[Effect] | None) -> "FunctionType": + def with_effects(self, max_effects_declared: list[Effect] | None) -> "FunctionType": """Returns a copy of this function type with the specified max_effects.""" # N.B. we can't use `dataclasses.replace` here since `FunctionType` has a custom # constructor - if self.max_effects is not None: + if self.max_effects_declared is not None: raise InternalGuppyError( "Tried to set max_effects on a FunctionType that already has them" ) @@ -620,7 +631,7 @@ def with_effects(self, max_effects: list[Effect] | None) -> "FunctionType": self.params, self.comptime_args, self.unitary_flags, - max_effects=max_effects, + max_effects_declared=max_effects_declared, ) diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 86008d3eb..02b29b21b 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:5:10) 3 | @guppy(max_effects=[]) 4 | def main(impure_f: Callable[[int], int]) -> int: 5 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -> int` has effects that - | exceed those allowed + | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed + | those allowed Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 3e0ba947e..91f5df0ca 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(max_effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects that - | exceed those allowed + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed + | those allowed Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index a60558830..5173516f5 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(max_effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects that - | exceed those allowed + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed + | those allowed Note: | From 622cfbf1b61720d527601e996f7be5f4563c174b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 19:10:50 +0100 Subject: [PATCH 36/81] Always include allowed-effects in Context even when no decl; worsens error printing --- .../checker/cfg_checker.py | 4 ++-- .../src/guppylang_internals/checker/core.py | 2 +- .../checker/expr_checker.py | 4 +--- .../checker/func_checker.py | 20 ++++++------------- .../checker/modifier_checker.py | 2 +- .../definition/overloaded.py | 10 +++++----- .../guppylang_internals/tracing/function.py | 5 ++++- .../src/guppylang_internals/tys/parsing.py | 3 ++- 8 files changed, 22 insertions(+), 28 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 69f3a0552..61a09d572 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -77,7 +77,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], AstNode | None], first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -232,7 +232,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], AstNode | None], ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index c9c964ae5..a93a1e67a 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -448,7 +448,7 @@ class Context(NamedTuple): globals: Globals locals: Locals[str, Variable] generic_param_inst: dict[str, Argument] - max_effects_from: tuple[list[Effect], AstNode] | None = None + max_effects_from: tuple[list[Effect], AstNode | None] @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 8a2321a88..12d855da8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1254,9 +1254,7 @@ def check_comptime_arg( 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 ctx.max_effects_from is not None and ( - any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) - ): + if any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects): loc_node = node.func if isinstance(node, ast.Call) else node effects_allowed, effects_decl = ctx.max_effects_from raise GuppyTypeError( diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 38c69ba14..8a7b98b75 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -22,10 +22,9 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.diagnostic import Error, Help, Note from guppylang_internals.engine import DEF_STORE, ENGINE -from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.error import GuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef -from guppylang_internals.tys import Effect from guppylang_internals.tys.parsing import ( TypeParsingCtx, check_function_arg, @@ -157,16 +156,9 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } - if ty.max_effects_declared is None: - max_effects_from = None - else: - dec = _find_guppy_decorator(func_def.decorator_list) - if dec is None and ty.max_effects_declared is not None: - raise InternalGuppyError( - f"Effects limited to {Effect.format_list(ty.max_effects_declared)}" - " but cannot identify decorator imposing this limit" - ) - max_effects_from = (ty.max_effects_declared, dec) + + dec = _find_guppy_decorator(func_def.decorator_list) + return check_cfg( cfg, inputs, @@ -174,7 +166,7 @@ def check_global_func_def( generic_args, func_def.name, globals, - max_effects_from=max_effects_from, + max_effects_from=(ty.max_effects, dec), ) @@ -202,7 +194,7 @@ def check_nested_func_def( # 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. func_ty = check_signature(func_def, ctx.globals).with_effects( - None if ctx.max_effects_from is None else ctx.max_effects_from[0] + ctx.max_effects_from[0] ) assert func_ty.input_names is not None diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index e43abf734..4a8f2e49a 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -24,7 +24,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], AstNode | None], ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index ee4d46569..60de5efb2 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -39,7 +39,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None - max_effects_from: tuple[list[Effect], AstNode] | None + max_effects_from: tuple[list[Effect], AstNode | None] @property def rendered_span_label(self) -> str: @@ -54,10 +54,10 @@ def rendered_span_label(self) -> str: stem += f"takes arguments {args}" if self.return_ty: stem += f" and returns `{self.return_ty}`" - if self.max_effects_from: - effects, _node = self.max_effects_from - if Effect.ANY not in effects: - stem += f" with effects no more than {effects}" + effects, _node = self.max_effects_from + if Effect.ANY not in effects: + effect_names = Effect.format_list(effects) + stem += f" with effects no more than {effect_names}" return stem diff --git a/guppylang-internals/src/guppylang_internals/tracing/function.py b/guppylang-internals/src/guppylang_internals/tracing/function.py index 22a46d7c8..e8a4c4fee 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/function.py +++ b/guppylang-internals/src/guppylang_internals/tracing/function.py @@ -37,6 +37,7 @@ update_packed_value, ) from guppylang_internals.tracing.util import capture_guppy_errors, tracing_except_hook +from guppylang_internals.tys import Effect from guppylang_internals.tys.ty import ( FunctionType, InputFlags, @@ -183,7 +184,9 @@ def trace_call(func: CallableDef, *args: Any) -> Any: with_loc(state.node, with_type(var.ty, PlaceNode(var))) for var in arg_vars ] # ALAN add max_effects to Tracing? - ctx = Context(Globals(DEF_STORE.frames[func.id]), locals, {}) + ctx = Context( + Globals(DEF_STORE.frames[func.id]), locals, {}, ([Effect.ANY], None) + ) call_node, ret_ty = func.synthesize_call(arg_exprs, state.node, ctx) # Here we check if unitary constraints are respected by the caller diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 33ee006c9..f5dcf729f 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -131,7 +131,8 @@ def arg_from_ast(node: AstNode, ctx: TypeParsingCtx) -> Argument: if comptime_expr := is_comptime_expression(node): from guppylang_internals.checker.expr_checker import eval_comptime_expr - v = eval_comptime_expr(comptime_expr, Context(ctx.globals, Locals({}), {})) + fx = ([Effect.ANY], None) # Or no effects? + v = eval_comptime_expr(comptime_expr, Context(ctx.globals, Locals({}), {}, fx)) if isinstance(v, int): nat_ty = NumericType(NumericType.Kind.Nat) return ConstArg(ConstValue(nat_ty, v)) From 078c2c84cf81bb09f552283292488a07f1dfd5c7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 19:10:56 +0100 Subject: [PATCH 37/81] Revert "Always include allowed-effects in Context even when no decl; worsens error printing" This reverts commit d47f6f8f08904499427831c76b8331697dda7210. --- .../checker/cfg_checker.py | 4 ++-- .../src/guppylang_internals/checker/core.py | 2 +- .../checker/expr_checker.py | 4 +++- .../checker/func_checker.py | 20 +++++++++++++------ .../checker/modifier_checker.py | 2 +- .../definition/overloaded.py | 10 +++++----- .../guppylang_internals/tracing/function.py | 5 +---- .../src/guppylang_internals/tys/parsing.py | 3 +-- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 61a09d572..69f3a0552 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -77,7 +77,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects_from: tuple[list[Effect], AstNode | None], + max_effects_from: tuple[list[Effect], AstNode] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -232,7 +232,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects_from: tuple[list[Effect], AstNode | None], + max_effects_from: tuple[list[Effect], AstNode] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index a93a1e67a..c9c964ae5 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -448,7 +448,7 @@ class Context(NamedTuple): globals: Globals locals: Locals[str, Variable] generic_param_inst: dict[str, Argument] - max_effects_from: tuple[list[Effect], AstNode | None] + max_effects_from: tuple[list[Effect], AstNode] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 12d855da8..8a2321a88 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1254,7 +1254,9 @@ def check_comptime_arg( 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 any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects): + if ctx.max_effects_from is not None and ( + any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) + ): loc_node = node.func if isinstance(node, ast.Call) else node effects_allowed, effects_decl = ctx.max_effects_from raise GuppyTypeError( diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 8a7b98b75..38c69ba14 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -22,9 +22,10 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.diagnostic import Error, Help, Note from guppylang_internals.engine import DEF_STORE, ENGINE -from guppylang_internals.error import GuppyError +from guppylang_internals.error import GuppyError, InternalGuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef +from guppylang_internals.tys import Effect from guppylang_internals.tys.parsing import ( TypeParsingCtx, check_function_arg, @@ -156,9 +157,16 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } - - dec = _find_guppy_decorator(func_def.decorator_list) - + if ty.max_effects_declared is None: + max_effects_from = None + else: + dec = _find_guppy_decorator(func_def.decorator_list) + if dec is None and ty.max_effects_declared is not None: + raise InternalGuppyError( + f"Effects limited to {Effect.format_list(ty.max_effects_declared)}" + " but cannot identify decorator imposing this limit" + ) + max_effects_from = (ty.max_effects_declared, dec) return check_cfg( cfg, inputs, @@ -166,7 +174,7 @@ def check_global_func_def( generic_args, func_def.name, globals, - max_effects_from=(ty.max_effects, dec), + max_effects_from=max_effects_from, ) @@ -194,7 +202,7 @@ def check_nested_func_def( # 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. func_ty = check_signature(func_def, ctx.globals).with_effects( - ctx.max_effects_from[0] + None if ctx.max_effects_from is None else ctx.max_effects_from[0] ) assert func_ty.input_names is not None diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index 4a8f2e49a..e43abf734 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -24,7 +24,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects_from: tuple[list[Effect], AstNode | None], + max_effects_from: tuple[list[Effect], AstNode] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index 60de5efb2..ee4d46569 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -39,7 +39,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None - max_effects_from: tuple[list[Effect], AstNode | None] + max_effects_from: tuple[list[Effect], AstNode] | None @property def rendered_span_label(self) -> str: @@ -54,10 +54,10 @@ def rendered_span_label(self) -> str: stem += f"takes arguments {args}" if self.return_ty: stem += f" and returns `{self.return_ty}`" - effects, _node = self.max_effects_from - if Effect.ANY not in effects: - effect_names = Effect.format_list(effects) - stem += f" with effects no more than {effect_names}" + if self.max_effects_from: + effects, _node = self.max_effects_from + if Effect.ANY not in effects: + stem += f" with effects no more than {effects}" return stem diff --git a/guppylang-internals/src/guppylang_internals/tracing/function.py b/guppylang-internals/src/guppylang_internals/tracing/function.py index e8a4c4fee..22a46d7c8 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/function.py +++ b/guppylang-internals/src/guppylang_internals/tracing/function.py @@ -37,7 +37,6 @@ update_packed_value, ) from guppylang_internals.tracing.util import capture_guppy_errors, tracing_except_hook -from guppylang_internals.tys import Effect from guppylang_internals.tys.ty import ( FunctionType, InputFlags, @@ -184,9 +183,7 @@ def trace_call(func: CallableDef, *args: Any) -> Any: with_loc(state.node, with_type(var.ty, PlaceNode(var))) for var in arg_vars ] # ALAN add max_effects to Tracing? - ctx = Context( - Globals(DEF_STORE.frames[func.id]), locals, {}, ([Effect.ANY], None) - ) + ctx = Context(Globals(DEF_STORE.frames[func.id]), locals, {}) call_node, ret_ty = func.synthesize_call(arg_exprs, state.node, ctx) # Here we check if unitary constraints are respected by the caller diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index f5dcf729f..33ee006c9 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -131,8 +131,7 @@ def arg_from_ast(node: AstNode, ctx: TypeParsingCtx) -> Argument: if comptime_expr := is_comptime_expression(node): from guppylang_internals.checker.expr_checker import eval_comptime_expr - fx = ([Effect.ANY], None) # Or no effects? - v = eval_comptime_expr(comptime_expr, Context(ctx.globals, Locals({}), {}, fx)) + v = eval_comptime_expr(comptime_expr, Context(ctx.globals, Locals({}), {})) if isinstance(v, int): nat_ty = NumericType(NumericType.Kind.Nat) return ConstArg(ConstValue(nat_ty, v)) From 2aae8da9c219657fa8865649e2db9ca4396ea618 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 19:46:12 +0100 Subject: [PATCH 38/81] Allow ANY in frontend, inc for Callables; test [ANY] vs no annotation --- guppylang/src/guppylang/decorator.py | 8 +- .../pure_calls_explicit_callable.err | 15 +++ .../pure_calls_explicit_callable.py | 7 ++ .../pure_calls_explicit_decl.err | 15 +++ .../pure_calls_explicit_decl.py | 10 ++ .../pure_calls_explicit_def.err | 15 +++ .../effects_errors/pure_calls_explicit_def.py | 11 +++ .../return_explicit_callable.err | 9 ++ .../return_explicit_callable.py | 11 +++ tests/integration/test_effects.py | 99 ++++++++++++++++++- 10 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 tests/error/effects_errors/pure_calls_explicit_callable.err create mode 100644 tests/error/effects_errors/pure_calls_explicit_callable.py create mode 100644 tests/error/effects_errors/pure_calls_explicit_decl.err create mode 100644 tests/error/effects_errors/pure_calls_explicit_decl.py create mode 100644 tests/error/effects_errors/pure_calls_explicit_def.err create mode 100644 tests/error/effects_errors/pure_calls_explicit_def.py create mode 100644 tests/error/effects_errors/return_explicit_callable.err create mode 100644 tests/error/effects_errors/return_explicit_callable.py diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 3e2821d86..7b6dba382 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -99,14 +99,14 @@ class Effect(Enum): - # No instances yet. - names = () + ANY = "ANY" def to_internal(self) -> _Effect: match self: + case Effect.ANY: + return _Effect.ANY case _ as unreachable: - # Seems assert_never doesn't handle Enum's without cases yet - assert_never(unreachable) # type: ignore[arg-type] + assert_never(unreachable) class GuppyKwargs(TypedDict, total=False): diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err new file mode 100644 index 000000000..dc2d0dd3a --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:5:10) + | +3 | @guppy(max_effects=[]) +4 | def main(impure_f: Callable[[int], int, [ANY]]) -> int: +5 | return impure_f(5) + | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that + | exceed those allowed + +Note: + | +2 | +3 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.py b/tests/error/effects_errors/pure_calls_explicit_callable.py new file mode 100644 index 000000000..4e8ab39a6 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_callable.py @@ -0,0 +1,7 @@ +from guppylang.decorator import guppy + +@guppy(max_effects=[]) +def main(impure_f: Callable[[int], int, [ANY]]) -> int: + return impure_f(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err new file mode 100644 index 000000000..b905c75ba --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:8:10) + | +6 | @guppy(max_effects=[]) +7 | def main() -> int: +8 | return impure_func(5) + | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that + | exceed those allowed + +Note: + | +5 | +6 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.py b/tests/error/effects_errors/pure_calls_explicit_decl.py new file mode 100644 index 000000000..a614b60ed --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_decl.py @@ -0,0 +1,10 @@ +from guppylang.decorator import guppy, Effect + +@guppy.declare(max_effects=[Effect.ANY]) +def impure_func(x: int) -> int: ... + +@guppy(max_effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err new file mode 100644 index 000000000..617b10b0f --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(max_effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that + | exceed those allowed + +Note: + | +6 | +7 | @guppy(max_effects=[]) + | --------------------- Allowed effects `[]` declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_def.py b/tests/error/effects_errors/pure_calls_explicit_def.py new file mode 100644 index 000000000..c94731455 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_def.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy, Effect + +@guppy(max_effects=[Effect.ANY]) +def impure_func(x: int) -> int: + return x + 1 + +@guppy(max_effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/return_explicit_callable.err b/tests/error/effects_errors/return_explicit_callable.err new file mode 100644 index 000000000..7d32fe210 --- /dev/null +++ b/tests/error/effects_errors/return_explicit_callable.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:9:10) + | +7 | @guppy +8 | def main() -> Callable[[int], int, []]: +9 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int + | -[ANY]-> int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_explicit_callable.py b/tests/error/effects_errors/return_explicit_callable.py new file mode 100644 index 000000000..55ad916c7 --- /dev/null +++ b/tests/error/effects_errors/return_explicit_callable.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy, Effect + +@guppy(max_effects=[Effect.ANY]) +def impure_func(x: int) -> int: + return x + 1 + +@guppy +def main() -> Callable[[int], int, []]: + return impure_func + +main.compile() diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index 9399e8333..b4f436008 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -1,8 +1,10 @@ """Tests of max_effects annotation.""" +import pytest from collections.abc import Callable -from guppylang.decorator import guppy +from guppylang.decorator import guppy, Effect +from guppylang.std.builtins import result def test_pure_decl_from_impure(validate): @@ -16,6 +18,17 @@ def impure_func(x: int) -> int: validate(impure_func.compile_function()) +def test_pure_decl_from_explicit_impure(validate): + @guppy.declare(max_effects=[]) + def pure_func(x: int) -> int: ... + + @guppy(max_effects=[Effect.ANY]) + def impure_func(x: int) -> int: + return pure_func(x) + 1 + + validate(impure_func.compile_function()) + + def test_pure_decl_from_pure(validate): @guppy.declare(max_effects=[]) def pure_func1(x: int) -> int: ... @@ -27,6 +40,25 @@ def pure_func2(x: int) -> int: validate(pure_func2.compile_function()) +@pytest.mark.parametrize( + ("caller", "callee"), + [ + ({"max_effects": [Effect.ANY]}, {}), + ({}, {"max_effects": [Effect.ANY]}), + ({"max_effects": [Effect.ANY]}, {"max_effects": [Effect.ANY]}), + ], +) +def test_impure_decl_explicit(caller, callee, validate): + @guppy.declare(**callee) + def impure_func(x: int) -> int: ... + + @guppy(**caller) + def impure_func2(x: int) -> int: + return impure_func(x) + 1 + + validate(impure_func2.compile_function()) + + def test_pure_from_impure(validate): @guppy(max_effects=[]) def pure_func(x: int) -> int: @@ -39,6 +71,39 @@ def normal_func(x: int) -> int: validate(normal_func.compile_function()) +def test_pure_from_explicit_impure(validate): + @guppy(max_effects=[]) + def pure_func(x: int) -> int: + return x + 1 + + @guppy(max_effects=[Effect.ANY]) + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +@pytest.mark.parametrize( + ("caller", "callee"), + [ + ({"max_effects": [Effect.ANY]}, {}), + ({}, {"max_effects": [Effect.ANY]}), + ({"max_effects": [Effect.ANY]}, {"max_effects": [Effect.ANY]}), + ], +) +def test_impure_explicit(caller, callee, validate): + @guppy(**callee) + def impure_func(x: int) -> int: + result("tag", x) + return x + 3 + + @guppy(**caller) + def impure_func2(x: int) -> int: + return impure_func(x) + 1 + + validate(impure_func2.compile_function()) + + def test_pure_from_pure(validate): @guppy(max_effects=[]) def pure_func1(x: int) -> int: @@ -65,3 +130,35 @@ def pure_func(pure_f: Callable[[int], int, []]) -> int: return pure_f(5) + 1 validate(pure_func.compile_function()) + + +def test_pure_callable_from_impure_explicit(validate): + @guppy(max_effects=[Effect.ANY]) + def impure_func(pure_f: Callable[[int], int, []]) -> int: + return pure_f(5) + 1 + + validate(impure_func.compile_function()) + + +def test_return_callable1(validate): + @guppy + def impure_func(x: int) -> int: + return x + 1 + + @guppy(max_effects=[]) + def higher_order() -> Callable[[int], int, [ANY]]: # noqa: F821 + return impure_func + + validate(higher_order.compile_function()) + + +def test_return_callable2(validate): + @guppy(max_effects=[Effect.ANY]) + def explicit_impure_func(x: int) -> int: + return x + 1 + + @guppy(max_effects=[]) + def higher_order() -> Callable[[int], int]: + return explicit_impure_func + + validate(higher_order.compile_function()) From 5c761ed5da316647f81014cb08b67e248a182053 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 21:08:53 +0100 Subject: [PATCH 39/81] Re-export guppylang.Effect, switch hugr_op/custom_func decorators --- guppylang-internals/src/guppylang_internals/decorator.py | 7 +++++-- guppylang/src/guppylang/__init__.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index f1e1f3980..c03e653f2 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -59,7 +59,8 @@ from collections.abc import Callable, Sequence from types import FrameType - from guppylang_internals.tys import Effect + from guppylang import Effect + from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst @@ -114,7 +115,9 @@ def dec(f: Callable[P, T]) -> GuppyFunctionDefinition[P, T]: signature, unitary_flags, has_var_args, - max_effects=max_effects, + max_effects=None + if max_effects is None + else [e.to_internal() for e in max_effects], ) DEF_STORE.register_def(func, get_calling_frame()) return GuppyFunctionDefinition(func) diff --git a/guppylang/src/guppylang/__init__.py b/guppylang/src/guppylang/__init__.py index bb2443afa..53b345fb3 100644 --- a/guppylang/src/guppylang/__init__.py +++ b/guppylang/src/guppylang/__init__.py @@ -1,12 +1,13 @@ from guppylang_internals.experimental import enable_experimental_features -from guppylang.decorator import guppy +from guppylang.decorator import Effect, guppy from guppylang.module import GuppyModule from guppylang.std import builtins, debug, quantum from guppylang.std.builtins import array, comptime, py from guppylang.std.quantum import qubit __all__ = ( + "Effect", "GuppyModule", "array", "builtins", From af1b7f28cd4907732edbb425140ef8f1c8afe766 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 21:10:38 +0100 Subject: [PATCH 40/81] num.py: label funcs as max_effects=[], except some panicking, and bool conversions --- guppylang/src/guppylang/std/num.py | 239 ++++++++++++++++------------- 1 file changed, 132 insertions(+), 107 deletions(-) diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 1ab6bdf3c..0ef5806a0 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -20,20 +20,20 @@ ) from guppylang_internals.tys.builtin import float_type_def, int_type_def, nat_type_def -from guppylang import guppy +from guppylang import Effect, guppy @extend_type(nat_type_def) class nat: """A 64-bit unsigned integer.""" - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __abs__(self: nat) -> nat: ... - @hugr_op(int_op("iadd")) + @hugr_op(int_op("iadd"), max_effects=[]) def __add__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("iand")) + @hugr_op(int_op("iand"), max_effects=[]) def __and__(self: nat, other: nat) -> nat: ... @guppy @@ -41,73 +41,78 @@ def __and__(self: nat, other: nat) -> nat: ... def __bool__(self: nat) -> bool: return self != 0 - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __ceil__(self: nat) -> nat: ... - @hugr_op(int_op("idivmod_u", n_vars=2)) + # Panics if other == 0 + @hugr_op(int_op("idivmod_u", n_vars=2), max_effects=[Effect.ANY]) def __divmod__(self: nat, other: nat) -> tuple[nat, nat]: ... - @custom_function(BoolOpCompiler(int_op("ieq"))) + @custom_function(BoolOpCompiler(int_op("ieq")), max_effects=[]) def __eq__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION)) + @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), max_effects=[]) def __float__(self: nat) -> float: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __floor__(self: nat) -> nat: ... - @hugr_op(int_op("idiv_u")) + # Panics if other == 0 + @hugr_op(int_op("idiv_u"), max_effects=[Effect.ANY]) def __floordiv__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ige_u"))) + @custom_function(BoolOpCompiler(int_op("ige_u")), max_effects=[]) def __ge__(self: nat, other: nat) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_u"))) + @custom_function(BoolOpCompiler(int_op("igt_u")), max_effects=[]) def __gt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("iu_to_s")) + @hugr_op(int_op("iu_to_s"), max_effects=[]) def __int__(self: nat) -> int: ... - @hugr_op(int_op("inot")) + @hugr_op(int_op("inot"), max_effects=[]) def __invert__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ile_u"))) + @custom_function(BoolOpCompiler(int_op("ile_u")), max_effects=[]) def __le__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("ishl")) + @hugr_op(int_op("ishl"), max_effects=[]) def __lshift__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ilt_u"))) + @custom_function(BoolOpCompiler(int_op("ilt_u")), max_effects=[]) def __lt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("imod_u")) + # Panics if other == 0 + @hugr_op(int_op("imod_u"), max_effects=[Effect.ANY]) def __mod__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("imul")) + @hugr_op(int_op("imul"), max_effects=[]) def __mul__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __nat__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine"))) + @custom_function(BoolOpCompiler(int_op("ine")), max_effects=[]) def __ne__(self: nat, other: nat) -> bool: ... - @custom_function(checker=DunderChecker("__nat__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__nat__"), higher_order_value=False, max_effects=[] + ) def __new__(x): ... - @hugr_op(int_op("ior")) + @hugr_op(int_op("ior"), max_effects=[]) def __or__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __pos__(self: nat) -> nat: ... - @hugr_op(int_op("ipow")) + @hugr_op(int_op("ipow"), max_effects=[]) def __pow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __radd__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rand__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) @@ -116,40 +121,40 @@ def __rdivmod__(self: nat, other: nat) -> tuple[nat, nat]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rlshift__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rmul__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __ror__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __round__(self: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rpow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rrshift__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("ishr")) + @hugr_op(int_op("ishr"), max_effects=[]) def __rshift__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rsub__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rtruediv__(self: nat, other: nat) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rxor__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("isub")) + @hugr_op(int_op("isub"), max_effects=[]) def __sub__(self: nat, other: nat) -> nat: ... @guppy @@ -157,10 +162,10 @@ def __sub__(self: nat, other: nat) -> nat: ... def __truediv__(self: nat, other: nat) -> float: return float(self) / float(other) - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __trunc__(self: nat) -> nat: ... - @hugr_op(int_op("ixor")) + @hugr_op(int_op("ixor"), max_effects=[]) def __xor__(self: nat, other: nat) -> nat: ... @@ -168,13 +173,16 @@ def __xor__(self: nat, other: nat) -> nat: ... class int: """A 64-bit signed integer.""" - @hugr_op(int_op("iabs")) # TODO: Maybe wrong? (signed vs unsigned!) + @hugr_op( + int_op("iabs"), # TODO: Maybe wrong? (signed vs unsigned!) + max_effects=[], + ) def __abs__(self: int) -> int: ... @hugr_op(int_op("iadd"), max_effects=[]) def __add__(self: int, other: int) -> int: ... - @hugr_op(int_op("iand")) + @hugr_op(int_op("iand"), max_effects=[]) def __and__(self: int, other: int) -> int: ... @guppy @@ -182,37 +190,39 @@ def __and__(self: int, other: int) -> int: ... def __bool__(self: int) -> bool: return self != 0 - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __ceil__(self: int) -> int: ... - @hugr_op(int_op("idivmod_s")) + # Panics if other == 0 + @hugr_op(int_op("idivmod_s"), max_effects=[Effect.ANY]) def __divmod__(self: int, other: int) -> tuple[int, int]: ... - @custom_function(BoolOpCompiler(int_op("ieq"))) + @custom_function(BoolOpCompiler(int_op("ieq")), max_effects=[]) def __eq__(self: int, other: int) -> bool: ... - @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION)) + @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), max_effects=[]) def __float__(self: int) -> float: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __floor__(self: int) -> int: ... - @hugr_op(int_op("idiv_s")) + # Panics if other == 0 + @hugr_op(int_op("idiv_s"), max_effects=[Effect.ANY]) def __floordiv__(self: int, other: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ige_s"))) + @custom_function(BoolOpCompiler(int_op("ige_s")), max_effects=[]) def __ge__(self: int, other: int) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_s"))) + @custom_function(BoolOpCompiler(int_op("igt_s")), max_effects=[]) def __gt__(self: int, other: int) -> bool: ... @custom_function(NoopCompiler()) def __int__(self: int) -> int: ... - @hugr_op(int_op("inot")) + @hugr_op(int_op("inot"), max_effects=[]) def __invert__(self: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ile_s"))) + @custom_function(BoolOpCompiler(int_op("ile_s")), max_effects=[]) def __le__(self: int, other: int) -> bool: ... @hugr_op(int_op("ishl")) # TODO: RHS is unsigned @@ -221,28 +231,30 @@ def __lshift__(self: int, other: int) -> int: ... @custom_function(BoolOpCompiler(int_op("ilt_s"))) def __lt__(self: int, other: int) -> bool: ... - @hugr_op(int_op("imod_s")) + @hugr_op(int_op("imod_s"), max_effects=[]) def __mod__(self: int, other: int) -> int: ... - @hugr_op(int_op("imul")) + @hugr_op(int_op("imul"), max_effects=[]) def __mul__(self: int, other: int) -> int: ... - @hugr_op(int_op("is_to_u")) + @hugr_op(int_op("is_to_u"), max_effects=[]) def __nat__(self: int) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine"))) + @custom_function(BoolOpCompiler(int_op("ine")), max_effects=[]) def __ne__(self: int, other: int) -> bool: ... - @hugr_op(int_op("ineg")) + @hugr_op(int_op("ineg"), max_effects=[]) def __neg__(self: int) -> int: ... - @custom_function(checker=DunderChecker("__int__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__int__"), higher_order_value=False, max_effects=[] + ) def __new__(x): ... - @hugr_op(int_op("ior")) + @hugr_op(int_op("ior"), max_effects=[]) def __or__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __pos__(self: int) -> int: ... @guppy @@ -255,13 +267,13 @@ def __pow__(self: int, exponent: int) -> int: ) return self.__pow_impl(exponent) - @hugr_op(int_op("ipow")) + @hugr_op(int_op("ipow"), max_effects=[]) # Exponent is treated as unsigned def __pow_impl(self: int, exponent: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __radd__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rand__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -270,19 +282,23 @@ def __rdivmod__(self: int, other: int) -> tuple[int, int]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned + @custom_function( + checker=ReversingChecker(), # TODO: RHS is unsigned + max_effects=[], + ) def __rlshift__(self: int, other: int) -> int: ... + # What if other==0 ? @custom_function(checker=ReversingChecker()) def __rmod__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rmul__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __ror__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __round__(self: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -291,30 +307,33 @@ def __rpow__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rrshift__(self: int, other: int) -> int: ... - @hugr_op(int_op("ishr")) # TODO: RHS is unsigned + @hugr_op( + int_op("ishr"), # TODO: RHS is unsigned + max_effects=[], + ) def __rshift__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rsub__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rtruediv__(self: int, other: int) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rxor__(self: int, other: int) -> int: ... - @hugr_op(int_op("isub")) + @hugr_op(int_op("isub"), max_effects=[]) def __sub__(self: int, other: int) -> int: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __truediv__(self: int, other: int) -> float: return float(self) / float(other) - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __trunc__(self: int) -> int: ... - @hugr_op(int_op("ixor")) + @hugr_op(int_op("ixor"), max_effects=[]) def __xor__(self: int, other: int) -> int: ... @@ -322,10 +341,10 @@ def __xor__(self: int, other: int) -> int: ... class float: """An IEEE754 double-precision floating point value.""" - @hugr_op(float_op("fabs")) + @hugr_op(float_op("fabs"), max_effects=[]) def __abs__(self: float) -> float: ... - @hugr_op(float_op("fadd")) + @hugr_op(float_op("fadd"), max_effects=[]) def __add__(self: float, other: float) -> float: ... @guppy @@ -333,113 +352,119 @@ def __add__(self: float, other: float) -> float: ... def __bool__(self: float) -> bool: return self != 0.0 - @hugr_op(float_op("fceil")) + @hugr_op(float_op("fceil"), max_effects=[]) def __ceil__(self: float) -> float: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __divmod__(self: float, other: float) -> tuple[float, float]: return self // other, self.__mod__(other) - @custom_function(BoolOpCompiler(float_op("feq"))) + @custom_function(BoolOpCompiler(float_op("feq")), max_effects=[]) def __eq__(self: float, other: float) -> bool: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __float__(self: float) -> float: ... - @hugr_op(float_op("ffloor")) + @hugr_op(float_op("ffloor"), max_effects=[]) def __floor__(self: float) -> float: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __floordiv__(self: float, other: float) -> float: return (self / other).__floor__() - @custom_function(BoolOpCompiler(float_op("fge"))) + @custom_function(BoolOpCompiler(float_op("fge")), max_effects=[]) def __ge__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("fgt"))) + @custom_function(BoolOpCompiler(float_op("fgt")), max_effects=[]) def __gt__(self: float, other: float) -> bool: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_s", hugr.std.int.CONVERSIONS_EXTENSION), - ) + ), + max_effects=[], ) def __int__(self: float) -> int: ... - @custom_function(BoolOpCompiler(float_op("fle"))) + @custom_function(BoolOpCompiler(float_op("fle")), max_effects=[]) def __le__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("flt"))) + @custom_function(BoolOpCompiler(float_op("flt")), max_effects=[]) def __lt__(self: float, other: float) -> bool: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __mod__(self: float, other: float) -> float: return self - (self // other) * other - @hugr_op(float_op("fmul")) + @hugr_op(float_op("fmul"), max_effects=[]) def __mul__(self: float, other: float) -> float: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_u", hugr.std.int.CONVERSIONS_EXTENSION), - ) + ), + max_effects=[], ) def __nat__(self: float) -> nat: ... - @custom_function(BoolOpCompiler(float_op("fne"))) + @custom_function(BoolOpCompiler(float_op("fne")), max_effects=[]) def __ne__(self: float, other: float) -> bool: ... - @hugr_op(float_op("fneg")) + @hugr_op(float_op("fneg"), max_effects=[]) def __neg__(self: float) -> float: ... - @custom_function(checker=DunderChecker("__float__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__float__"), higher_order_value=False, max_effects=[] + ) def __new__(x): ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), max_effects=[]) def __pos__(self: float) -> float: ... - @hugr_op(float_op("fpow")) # TODO + @hugr_op(float_op("fpow"), max_effects=[]) # TODO def __pow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __radd__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rdivmod__(self: float, other: float) -> tuple[float, float]: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rfloordiv__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rmod__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rmul__(self: float, other: float) -> float: ... - @hugr_op(float_op("fround")) # TODO + @hugr_op(float_op("fround"), max_effects=[]) # TODO def __round__(self: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rpow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rsub__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), max_effects=[]) def __rtruediv__(self: float, other: float) -> float: ... - @hugr_op(float_op("fsub")) + @hugr_op(float_op("fsub"), max_effects=[]) def __sub__(self: float, other: float) -> float: ... - @hugr_op(float_op("fdiv")) + @hugr_op(float_op("fdiv"), max_effects=[]) def __truediv__(self: float, other: float) -> float: ... - @hugr_op(unsupported_op("trunc_s")) # TODO `trunc_s` returns an option + @hugr_op( + unsupported_op("trunc_s"), max_effects=[] + ) # TODO `trunc_s` returns an option def __trunc__(self: float) -> float: ... From 0173ac0206e29e477066682cee553e38298b99d1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 May 2026 21:25:09 +0100 Subject: [PATCH 41/81] conversion to bool with no effects - sadly a hard requirement for 'truthy' --- .../src/guppylang_internals/checker/expr_checker.py | 7 ++++++- guppylang/src/guppylang/std/num.py | 6 +++--- guppylang/src/guppylang/std/qsystem/__init__.py | 5 +++-- tests/error/type_errors/and_not_bool_left.err | 2 +- tests/error/type_errors/and_not_bool_right.err | 2 +- tests/error/type_errors/if_expr_not_bool.err | 2 +- tests/error/type_errors/if_not_bool.err | 2 +- tests/error/type_errors/not_not_bool.err | 2 +- tests/error/type_errors/or_not_bool_left.err | 2 +- tests/error/type_errors/or_not_bool_right.err | 2 +- tests/error/type_errors/while_not_bool.err | 2 +- 11 files changed, 20 insertions(+), 14 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 8a2321a88..a98eab3b0 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1453,6 +1453,9 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type return node, node_ty synth = ExprSynthesizer(ctx) exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Inout)], bool_type()) + # 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([]) try: return synth.synthesize_instance_func( node, [], "__bool__", "truthy", exp_sig, True @@ -1461,7 +1464,9 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type if not node_ty.copyable: # Linear types may implement a `__consume_as_bool__` method that consumes # the value, instead of borrowing it. - exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Owned)], bool_type()) + exp_sig = FunctionType( + [FuncInput(node_ty, InputFlags.Owned)], bool_type() + ).with_effects([]) return synth.synthesize_instance_func( node, [], "__consume_as_bool__", "truthy", exp_sig, True ) diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 0ef5806a0..26585f5b3 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -36,7 +36,7 @@ def __add__(self: nat, other: nat) -> nat: ... @hugr_op(int_op("iand"), max_effects=[]) def __and__(self: nat, other: nat) -> nat: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __bool__(self: nat) -> bool: return self != 0 @@ -185,7 +185,7 @@ def __add__(self: int, other: int) -> int: ... @hugr_op(int_op("iand"), max_effects=[]) def __and__(self: int, other: int) -> int: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __bool__(self: int) -> bool: return self != 0 @@ -347,7 +347,7 @@ def __abs__(self: float) -> float: ... @hugr_op(float_op("fadd"), max_effects=[]) def __add__(self: float, other: float) -> float: ... - @guppy + @guppy(max_effects=[]) @no_type_check def __bool__(self: float) -> bool: return self != 0.0 diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index b2ab03655..7529d8901 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -256,14 +256,15 @@ class Measurement: """Represents the result of a lazy measurement which needs to be explicitly read before being used.""" - @custom_function(compiler=ReadFutureBoolCompiler()) + # We do *not* model the pipeline as a side-effect + @custom_function(compiler=ReadFutureBoolCompiler(), max_effects=[]) @no_type_check def read(self: "Measurement" @ owned) -> bool: """Read the measurement result, consuming it. Blocks until the result is available if the measurement hasn't been performed yet since being requested. """ - @guppy + @guppy(max_effects=[]) @no_type_check def __consume_as_bool__(self: "Measurement" @ owned) -> bool: return self.read() diff --git a/tests/error/type_errors/and_not_bool_left.err b/tests/error/type_errors/and_not_bool_left.err index cc8839a67..01e5c670d 100644 --- a/tests/error/type_errors/and_not_bool_left.err +++ b/tests/error/type_errors/and_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_right.err b/tests/error/type_errors/and_not_bool_right.err index 52586da05..9af5981d3 100644 --- a/tests/error/type_errors/and_not_bool_right.err +++ b/tests/error/type_errors/and_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:17) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_expr_not_bool.err b/tests/error/type_errors/if_expr_not_bool.err index ffb3fb0f9..6b6dbdfe2 100644 --- a/tests/error/type_errors/if_expr_not_bool.err +++ b/tests/error/type_errors/if_expr_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return 1 if x else 0 | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_not_bool.err b/tests/error/type_errors/if_not_bool.err index b52e7b368..96ff71a86 100644 --- a/tests/error/type_errors/if_not_bool.err +++ b/tests/error/type_errors/if_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:7) 7 | if x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_not_bool.err b/tests/error/type_errors/not_not_bool.err index ab16adf98..8d9ad86b2 100644 --- a/tests/error/type_errors/not_not_bool.err +++ b/tests/error/type_errors/not_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:15) 7 | return not x | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_left.err b/tests/error/type_errors/or_not_bool_left.err index c17142398..bd8ce6c86 100644 --- a/tests/error/type_errors/or_not_bool_left.err +++ b/tests/error/type_errors/or_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_right.err b/tests/error/type_errors/or_not_bool_right.err index 4769de617..f4495645b 100644 --- a/tests/error/type_errors/or_not_bool_right.err +++ b/tests/error/type_errors/or_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/while_not_bool.err b/tests/error/type_errors/while_not_bool.err index 13469fd99..1dc04396f 100644 --- a/tests/error/type_errors/while_not_bool.err +++ b/tests/error/type_errors/while_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:10) 7 | while x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error From 645b10c5d3c5d5e3756b7dd9f6ca621f8c9cb675 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 13:02:37 +0100 Subject: [PATCH 42/81] Mark many quantum funcs are pure, also angle. But, breaks because angle.__new__ --- guppylang/src/guppylang/std/angles.py | 18 +++--- .../src/guppylang/std/quantum/__init__.py | 56 +++++++++++-------- .../src/guppylang/std/quantum/functional.py | 42 +++++++------- tests/error/modifier_errors/higher_order.err | 8 +-- tests/error/modifier_errors/higher_order.py | 5 +- tests/error/poly_errors/non_linear2.err | 11 ++-- tests/error/poly_errors/non_linear2.py | 4 +- 7 files changed, 77 insertions(+), 67 deletions(-) diff --git a/guppylang/src/guppylang/std/angles.py b/guppylang/src/guppylang/std/angles.py index d44aea0b0..b3fb7e4c2 100644 --- a/guppylang/src/guppylang/std/angles.py +++ b/guppylang/src/guppylang/std/angles.py @@ -31,47 +31,47 @@ class angle: halfturns: float - @guppy + @guppy(max_effects=[]) @no_type_check def __add__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns + other.halfturns) - @guppy + @guppy(max_effects=[]) @no_type_check def __sub__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns - other.halfturns) - @guppy + @guppy(max_effects=[]) @no_type_check def __mul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy + @guppy(max_effects=[]) @no_type_check def __rmul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy + @guppy(max_effects=[]) @no_type_check def __truediv__(self: "angle", other: float) -> "angle": return angle(self.halfturns / other) - @guppy + @guppy(max_effects=[]) @no_type_check def __rtruediv__(self: "angle", other: float) -> "angle": return angle(other / self.halfturns) - @guppy + @guppy(max_effects=[]) @no_type_check def __neg__(self: "angle") -> "angle": return angle(-self.halfturns) - @guppy + @guppy(max_effects=[]) @no_type_check def __float__(self: "angle") -> float: return self.halfturns * py(math.pi) - @guppy + @guppy(max_effects=[]) @no_type_check def __eq__(self: "angle", other: "angle") -> bool: return self.halfturns == other.halfturns diff --git a/guppylang/src/guppylang/std/quantum/__init__.py b/guppylang/src/guppylang/std/quantum/__init__.py index 38599c832..225dc5a35 100644 --- a/guppylang/src/guppylang/std/quantum/__init__.py +++ b/guppylang/src/guppylang/std/quantum/__init__.py @@ -26,12 +26,12 @@ class qubit: @no_type_check def __new__() -> "qubit": ... - @guppy + @guppy # not pure: this is measure+free @no_type_check def measure(self: "qubit" @ owned) -> bool: return measure(self) - @guppy + @guppy(max_effects=[]) @no_type_check def project_z(self: "qubit") -> bool: return project_z(self) @@ -49,7 +49,7 @@ def maybe_qubit() -> Option[qubit]: if allocation succeeds or `nothing` if it fails.""" -@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def h(q: qubit) -> None: r"""Hadamard gate command @@ -63,7 +63,7 @@ def h(q: qubit) -> None: """ -@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def cz(control: qubit, target: qubit) -> None: r"""Controlled-Z gate command. @@ -83,7 +83,7 @@ def cz(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def cy(control: qubit, target: qubit) -> None: r"""Controlled-Y gate command. @@ -103,7 +103,7 @@ def cy(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def cx(control: qubit, target: qubit) -> None: r"""Controlled-X gate command. @@ -123,7 +123,7 @@ def cx(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def t(q: qubit) -> None: r"""T gate. @@ -138,7 +138,7 @@ def t(q: qubit) -> None: """ -@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def s(q: qubit) -> None: r"""S gate. @@ -153,7 +153,7 @@ def s(q: qubit) -> None: """ -@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def v(q: qubit) -> None: r"""V gate. @@ -168,7 +168,7 @@ def v(q: qubit) -> None: """ -@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def x(q: qubit) -> None: r"""X gate. @@ -183,7 +183,7 @@ def x(q: qubit) -> None: """ -@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def y(q: qubit) -> None: r"""Y gate. @@ -198,7 +198,7 @@ def y(q: qubit) -> None: """ -@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def z(q: qubit) -> None: r"""Z gate. @@ -213,7 +213,7 @@ def z(q: qubit) -> None: """ -@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def tdg(q: qubit) -> None: r"""Tdg gate. @@ -228,7 +228,7 @@ def tdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def sdg(q: qubit) -> None: r"""Sdg gate. @@ -243,7 +243,7 @@ def sdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def vdg(q: qubit) -> None: r"""Vdg gate. @@ -258,7 +258,9 @@ def vdg(q: qubit) -> None: """ -@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary) +@custom_function( + RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] +) @no_type_check def rz(q: qubit, angle: angle) -> None: r"""Rz gate. @@ -274,7 +276,9 @@ def rz(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary) +@custom_function( + RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] +) @no_type_check def rx(q: qubit, angle: angle) -> None: r"""Rx gate. @@ -289,7 +293,9 @@ def rx(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary) +@custom_function( + RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] +) @no_type_check def ry(q: qubit, angle: angle) -> None: r"""Ry gate. @@ -304,7 +310,9 @@ def ry(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary) +@custom_function( + RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] +) @no_type_check def crz(control: qubit, target: qubit, angle: angle) -> None: r"""Controlled-Rz gate command. @@ -324,7 +332,7 @@ def crz(control: qubit, target: qubit, angle: angle) -> None: """ -@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) @no_type_check def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: r"""A Toffoli gate command. Also sometimes known as a CCX gate. @@ -348,7 +356,7 @@ def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: """ -@custom_function(InoutMeasureCompiler()) +@custom_function(InoutMeasureCompiler(), max_effects=[]) @no_type_check def project_z(q: qubit) -> bool: """Project a single qubit into the Z-basis (a non-destructive measurement).""" @@ -366,7 +374,7 @@ def measure(q: qubit @ owned) -> bool: """Measure a single qubit destructively.""" -@hugr_op(quantum_op("Reset")) +@hugr_op(quantum_op("Reset"), max_effects=[]) @no_type_check def reset(q: qubit) -> None: """Reset a single qubit to the :math:`|0\rangle` state.""" @@ -375,7 +383,7 @@ def reset(q: qubit) -> None: N = guppy.nat_var("N") -@guppy +@guppy # This does N calls to QFree, so it is not pure @no_type_check def measure_array(qubits: array[qubit, N] @ owned) -> array[bool, N]: """Measure an array of qubits, returning an array of bools.""" @@ -395,7 +403,7 @@ def discard_array(qubits: array[qubit, N] @ owned) -> None: # -------NON-PRIMITIVE------- -@guppy +@guppy(max_effects=[]) @no_type_check def ch(control: qubit, target: qubit) -> None: r"""Controlled-H gate command. diff --git a/guppylang/src/guppylang/std/quantum/functional.py b/guppylang/src/guppylang/std/quantum/functional.py index 58cebf98b..aeb0b524f 100644 --- a/guppylang/src/guppylang/std/quantum/functional.py +++ b/guppylang/src/guppylang/std/quantum/functional.py @@ -15,7 +15,7 @@ from guppylang.std.quantum import qubit -@guppy +@guppy(max_effects=[]) @no_type_check def h(q: qubit @ owned) -> qubit: """Functional Hadamard gate command.""" @@ -23,7 +23,7 @@ def h(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CZ gate command.""" @@ -31,7 +31,7 @@ def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(max_effects=[]) @no_type_check def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CX gate command.""" @@ -39,7 +39,7 @@ def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(max_effects=[]) @no_type_check def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CY gate command.""" @@ -47,7 +47,7 @@ def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(max_effects=[]) @no_type_check def t(q: qubit @ owned) -> qubit: """Functional T gate command.""" @@ -55,7 +55,7 @@ def t(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def s(q: qubit @ owned) -> qubit: """Functional S gate command.""" @@ -63,7 +63,7 @@ def s(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def v(q: qubit @ owned) -> qubit: """Functional V gate command.""" @@ -71,7 +71,7 @@ def v(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def x(q: qubit @ owned) -> qubit: """Functional X gate command.""" @@ -79,7 +79,7 @@ def x(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def y(q: qubit @ owned) -> qubit: """Functional Y gate command.""" @@ -87,7 +87,7 @@ def y(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def z(q: qubit @ owned) -> qubit: """Functional Z gate command.""" @@ -95,7 +95,7 @@ def z(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def tdg(q: qubit @ owned) -> qubit: """Functional Tdg gate command.""" @@ -103,7 +103,7 @@ def tdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def sdg(q: qubit @ owned) -> qubit: """Functional Sdg gate command.""" @@ -111,7 +111,7 @@ def sdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def vdg(q: qubit @ owned) -> qubit: """Functional Vdg gate command.""" @@ -119,7 +119,7 @@ def vdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def rz(q: qubit @ owned, angle: angle) -> qubit: """Functional Rz gate command.""" @@ -127,7 +127,7 @@ def rz(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def rx(q: qubit @ owned, angle: angle) -> qubit: """Functional Rx gate command.""" @@ -135,7 +135,7 @@ def rx(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def ry(q: qubit @ owned, angle: angle) -> qubit: """Functional Ry gate command.""" @@ -143,7 +143,7 @@ def ry(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def crz( control: qubit @ owned, target: qubit @ owned, angle: angle @@ -153,7 +153,7 @@ def crz( return control, target -@guppy +@guppy(max_effects=[]) @no_type_check def toffoli( control1: qubit @ owned, control2: qubit @ owned, target: qubit @ owned @@ -163,7 +163,7 @@ def toffoli( return control1, control2, target -@guppy +@guppy(max_effects=[]) @no_type_check def reset(q: qubit @ owned) -> qubit: """Functional Reset command.""" @@ -171,7 +171,7 @@ def reset(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(max_effects=[]) @no_type_check def project_z(q: qubit @ owned) -> tuple[qubit, bool]: """Functional project_z command.""" @@ -182,7 +182,7 @@ def project_z(q: qubit @ owned) -> tuple[qubit, bool]: # -------NON-PRIMITIVE------- -@guppy +@guppy(max_effects=[]) @no_type_check def ch(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional Controlled-H gate command.""" diff --git a/tests/error/modifier_errors/higher_order.err b/tests/error/modifier_errors/higher_order.err index 81770925f..ad2fe9de6 100644 --- a/tests/error/modifier_errors/higher_order.err +++ b/tests/error/modifier_errors/higher_order.err @@ -1,8 +1,8 @@ -Error: Dagger constraint violation (at $FILE:11:4) +Error: Dagger constraint violation (at $FILE:10:4) | - 9 | def test_ho(f: Callable[[qubit], None], q: qubit) -> None: -10 | # There is no way to use specify flags for f -11 | f(q) + 8 | @guppy(dagger=True) + 9 | def test_ho(f: Callable[[qubit], None, []], q: qubit) -> None: +10 | f(q) | ^^^^ This function cannot be called in a dagger context Guppy compilation failed due to 1 previous error diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index d35e25bcf..4b1ace90b 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -4,10 +4,9 @@ from collections.abc import Callable -# The flag is required to be used in dagger context +# f would need a flag to be used in dagger context, but no way to specify that yet @guppy(dagger=True) -def test_ho(f: Callable[[qubit], None], q: qubit) -> None: - # There is no way to use specify flags for f +def test_ho(f: Callable[[qubit], None, []], q: qubit) -> None: f(q) diff --git a/tests/error/poly_errors/non_linear2.err b/tests/error/poly_errors/non_linear2.err index 006751cdf..941c829fd 100644 --- a/tests/error/poly_errors/non_linear2.err +++ b/tests/error/poly_errors/non_linear2.err @@ -1,9 +1,10 @@ -Error: Not defined for linear argument (at $FILE:15:4) +Error: Not defined for linear argument (at $FILE:17:4) | -13 | @guppy -14 | def main() -> None: -15 | foo(h) +15 | @guppy +16 | def main() -> None: +17 | foo(h) | ^^^^^^ Cannot instantiate copyable type parameter `T` in type - | `forall T. (T -> T) -> None` with non-copyable type `qubit` + | `forall T. (T -[]-> T) -> None` with non-copyable type + | `qubit` Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index 7fb3d6e63..e640aca95 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -6,8 +6,10 @@ T = guppy.type_var("T") +# Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly +# declare the effects of `f` @guppy.declare -def foo(x: Callable[[T], T]) -> None: ... +def foo(x: Callable[[T], T, []]) -> None: ... @guppy From 339389d8a2ab2778d0030cc482b6c4baec95141d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 13:03:13 +0100 Subject: [PATCH 43/81] Add tests of higher-order effects (part-xfailed) --- tests/integration/test_higher_order.py | 43 +++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_higher_order.py b/tests/integration/test_higher_order.py index 0818e3949..9445399b9 100644 --- a/tests/integration/test_higher_order.py +++ b/tests/integration/test_higher_order.py @@ -1,6 +1,8 @@ +import pytest + from collections.abc import Callable -from guppylang.decorator import guppy +from guppylang.decorator import guppy, Effect from tests.util import compile_guppy @@ -174,3 +176,42 @@ def fac(x: int) -> int: return Y(fac_)(x) validate(fac.compile_function()) + + +# This should be combined with `test_higher_order_effects2` once we have solved +# https://github.com/Quantinuum/guppylang/issues/1760 +# but presently exists to show the part of the test that *does* work +def test_higher_order_effects1(validate): + @guppy(max_effects=[Effect.ANY]) + def impure_func(x: int) -> int: + return x + 1 + + # Same def as `test_higher_order_effects2` + @guppy + def higher_order(f: Callable[[int], int], x: int) -> int: + return f(x) + + @guppy + def main() -> int: + return higher_order(impure_func, 5) + + validate(main.compile_function()) + + +@pytest.mark.xfail(reason="Pending https://github.com/Quantinuum/guppylang/issues/1760") +def test_higher_order_effects2(validate): + @guppy(max_effects=[]) + def pure_func(x: int) -> int: + return x + 1 + + @guppy # we'd love this to be "as pure as f is", but no way to do that yet. + # (Alternatively https://github.com/Quantinuum/guppylang/issues/1752 will allow + # explicitly declaring such effect-polymorphism, but that won't parse yet) + def higher_order(f: Callable[[int], int], x: int) -> int: + return f(x) + + @guppy(max_effects=[]) + def main() -> int: + return higher_order(pure_func, 5) + + validate(main.compile_function()) From cba65d56c560a6364d5041c0607c9de58f7e7157 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 13:07:07 +0100 Subject: [PATCH 44/81] Mark default Struct constructor as no-effects, update error messages --- .../src/guppylang_internals/definition/struct.py | 1 + tests/error/struct_errors/constructor_missing_arg.err | 2 +- tests/error/struct_errors/constructor_too_many_args.err | 2 +- tests/integration/notebooks/misc_notebook_tests.ipynb | 2 +- tests/integration/test_struct.py | 4 +++- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index e58d5bbf0..7f6033383 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -228,6 +228,7 @@ def compile(self, args: list[Wire]) -> list[Wire]: defn=self, args=[p.to_bound(i) for i, p in enumerate(self.params)] ), params=self.params, + max_effects_declared=[], ) constructor_def = CustomFunctionDef( id=DefId.fresh(), diff --git a/tests/error/struct_errors/constructor_missing_arg.err b/tests/error/struct_errors/constructor_missing_arg.err index 1a673d9b8..9695292e0 100644 --- a/tests/error/struct_errors/constructor_missing_arg.err +++ b/tests/error/struct_errors/constructor_missing_arg.err @@ -5,6 +5,6 @@ Error: Not enough arguments (at $FILE:11:12) 11 | MyStruct() | ^^ Missing argument (expected 1, got 0) -Note: Function signature is `int -> MyStruct` +Note: Function signature is `int -[]-> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_too_many_args.err b/tests/error/struct_errors/constructor_too_many_args.err index 2e5f65d47..e993ba76b 100644 --- a/tests/error/struct_errors/constructor_too_many_args.err +++ b/tests/error/struct_errors/constructor_too_many_args.err @@ -5,6 +5,6 @@ Error: Too many arguments (at $FILE:11:16) 11 | MyStruct(1, 2, 3) | ^^^^ Unexpected arguments (expected 1, got 3) -Note: Function signature is `int -> MyStruct` +Note: Function signature is `int -[]-> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/integration/notebooks/misc_notebook_tests.ipynb b/tests/integration/notebooks/misc_notebook_tests.ipynb index 588d46b8b..097f4d4d8 100644 --- a/tests/integration/notebooks/misc_notebook_tests.ipynb +++ b/tests/integration/notebooks/misc_notebook_tests.ipynb @@ -137,7 +137,7 @@ "16 | return MyStruct()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -> MyStruct`\n", + "Note: Function signature is `int -[]-> MyStruct`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index 05a60537e..a6875c743 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -133,8 +133,10 @@ def test_higher_order(validate): class Struct(Generic[T]): x: T + # Pending https://github.com/Quantinuum/guppylang/issues/1760 + # we must explicitly state the effects of `mk_struct` @guppy - def factory(mk_struct: "Callable[[int], Struct[int]]", x: int) -> Struct[int]: + def factory(mk_struct: "Callable[[int], Struct[int], []]", x: int) -> Struct[int]: return mk_struct(x) @guppy From 9fabd5ccd21901a0305a9a2a66bb69bdb4b1cc33 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 15:48:52 +0100 Subject: [PATCH 45/81] max_effects => effects --- .../checker/expr_checker.py | 6 +- .../src/guppylang_internals/decorator.py | 10 +- .../guppylang_internals/definition/custom.py | 4 +- .../definition/declaration.py | 4 +- .../definition/function.py | 4 +- .../guppylang_internals/definition/traced.py | 6 +- .../guppylang_internals/tracing/function.py | 2 +- .../src/guppylang_internals/tys/parsing.py | 10 +- .../src/guppylang_internals/tys/ty.py | 12 +- guppylang/src/guppylang/decorator.py | 16 +- guppylang/src/guppylang/std/angles.py | 18 +- guppylang/src/guppylang/std/num.py | 222 +++++++++--------- .../src/guppylang/std/qsystem/__init__.py | 4 +- .../src/guppylang/std/quantum/__init__.py | 50 ++-- .../src/guppylang/std/quantum/functional.py | 42 ++-- tests/error/effects_errors/overload.err | 2 +- tests/error/effects_errors/overload.py | 6 +- .../pure_calls_explicit_callable.err | 4 +- .../pure_calls_explicit_callable.py | 2 +- .../pure_calls_explicit_decl.err | 4 +- .../pure_calls_explicit_decl.py | 4 +- .../pure_calls_explicit_def.err | 4 +- .../effects_errors/pure_calls_explicit_def.py | 4 +- .../pure_calls_impure_callable.err | 4 +- .../pure_calls_impure_callable.py | 2 +- .../effects_errors/pure_calls_impure_decl.err | 4 +- .../effects_errors/pure_calls_impure_decl.py | 2 +- .../effects_errors/pure_calls_impure_def.err | 4 +- .../effects_errors/pure_calls_impure_def.py | 2 +- tests/error/effects_errors/pure_result.err | 2 +- tests/error/effects_errors/pure_result.py | 2 +- .../return_explicit_callable.py | 2 +- .../effects_errors/return_pure_callable.py | 2 +- tests/integration/test_effects.py | 44 ++-- tests/integration/test_higher_order.py | 6 +- 35 files changed, 252 insertions(+), 264 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 6d29bbe4f..e9f1945cc 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1314,14 +1314,12 @@ 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 ctx.max_effects_from is not None and ( - any(e not in ctx.max_effects_from[0] for e in func_ty.max_effects) + any(e not in ctx.max_effects_from[0] for e in func_ty.effects) ): loc_node = node.func if isinstance(node, ast.Call) else node effects_allowed, effects_decl = ctx.max_effects_from raise GuppyTypeError( - TooManyEffectsError( - loc_node, func_ty, func_ty.max_effects - ).add_sub_diagnostic( + TooManyEffectsError(loc_node, func_ty, func_ty.effects).add_sub_diagnostic( TooManyEffectsError.MaxFromDecl(effects_decl, effects_allowed) ) ) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index c03e653f2..c0c5fa9c9 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -90,7 +90,7 @@ def custom_function( signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, has_var_args: bool = False, - max_effects: list[Effect] | None = None, + effects: list[Effect] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to add custom typing or compilation behaviour to function decls. @@ -115,9 +115,7 @@ def dec(f: Callable[P, T]) -> GuppyFunctionDefinition[P, T]: signature, unitary_flags, has_var_args, - max_effects=None - if max_effects is None - else [e.to_internal() for e in max_effects], + effects=None if effects is None else [e.to_internal() for e in effects], ) DEF_STORE.register_def(func, get_calling_frame()) return GuppyFunctionDefinition(func) @@ -132,7 +130,7 @@ def hugr_op( name: str = "", signature: FunctionType | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, - max_effects: list[Effect] | None = None, + effects: list[Effect] | None = None, ) -> Callable[[Callable[P, T]], GuppyFunctionDefinition[P, T]]: """Decorator to annotate function declarations as HUGR ops. @@ -151,7 +149,7 @@ def hugr_op( name, signature, unitary_flags=unitary_flags, - max_effects=max_effects, + effects=effects, ) diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index b6b814413..3f79b01df 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -127,7 +127,7 @@ class RawCustomFunctionDef(ParsableDef): # in Guppy functions in general but some custom functions make use of them). has_var_args: bool = field(default=False) - max_effects: list[Effect] | None = field(default=None, kw_only=True) + effects: list[Effect] | None = field(default=None, kw_only=True) description: str = field(default="function", init=False) @@ -152,7 +152,7 @@ def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef": raise GuppyError(BodyNotEmptyError(func_ast.body[0], self.name)) sig = self.signature or self._get_signature(func_ast, globals) ty = sig or FunctionType([], NoneType()) - ty = ty.with_unitary_flags(self.unitary_flags).with_effects(self.max_effects) + ty = ty.with_unitary_flags(self.unitary_flags).with_effects(self.effects) return CustomFunctionDef( self.id, self.name, diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index f6cf47657..06a992bb9 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -94,7 +94,7 @@ class RawFunctionDecl(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[Effect] | None = field(default=None, kw_only=True) + effects: list[Effect] | None = field(default=None, kw_only=True) @override def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": @@ -102,7 +102,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDecl": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature( func_ast, globals, self.id, unitary_flags=self.unitary_flags - ).with_effects(self.max_effects) + ).with_effects(self.effects) link_name = self._user_set_link_name or default_func_link_name(self) # TODO: For the guppylang 1.0 break, we should consider disallowing generic diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index f6899ca37..214f1d73d 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -119,7 +119,7 @@ class RawFunctionDef(ParsableDef, UserProvidedLinkName): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[Effect] | None = field(default=None, kw_only=True) + effects: list[Effect] | None = field(default=None, kw_only=True) @override def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": @@ -127,7 +127,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature( func_ast, globals, self.id, unitary_flags=self.unitary_flags - ).with_effects(self.max_effects) + ).with_effects(self.effects) link_name = self._user_set_link_name or default_func_link_name(self) return ParsedFunctionDef( diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index e75969e3e..8c5070474 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -57,7 +57,7 @@ class RawTracedFunctionDef(ParsableDef): metadata: FunctionMetadata | None = field(default=None, kw_only=True) - max_effects: list[Effect] | None = field(default=None, kw_only=True) + effects: list[Effect] | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": """Parses and checks the user-provided signature of the function.""" @@ -75,7 +75,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": self.python_func, unitary_flags=self.unitary_flags, metadata=self.metadata, - max_effects=self.max_effects, + effects=self.effects, ) @@ -135,7 +135,7 @@ def compile_outer( func_def, unitary_flags=self.unitary_flags, metadata=self.metadata, - max_effects=self.max_effects, + effects=self.effects, ) diff --git a/guppylang-internals/src/guppylang_internals/tracing/function.py b/guppylang-internals/src/guppylang_internals/tracing/function.py index 22a46d7c8..4677a0623 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/function.py +++ b/guppylang-internals/src/guppylang_internals/tracing/function.py @@ -182,7 +182,7 @@ def trace_call(func: CallableDef, *args: Any) -> Any: arg_exprs: list[ast.expr] = [ with_loc(state.node, with_type(var.ty, PlaceNode(var))) for var in arg_vars ] - # ALAN add max_effects to Tracing? + # ALAN add effects to Tracing? ctx = Context(Globals(DEF_STORE.frames[func.id]), locals, {}) call_node, ret_ty = func.synthesize_call(arg_exprs, state.node, ctx) diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index de45d0461..34775d330 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -290,21 +290,21 @@ def _parse_callable_type( inputs = [parse_function_arg_annotation(inp, None, ctx) for inp in inputs.elts] output = type_from_ast(output, ctx) - max_effects: list[Effect] | None + effects: list[Effect] | None if len(args) == 2: - max_effects = None + effects = None elif not isinstance(args[2], ast.List): raise GuppyError(err) else: - max_effects = [] + effects = [] for e in args[2].elts: if not isinstance(e, ast.Name): raise GuppyError(err) try: - max_effects.append(Effect.__from_str__(e.id)) + effects.append(Effect.__from_str__(e.id)) except ValueError: raise GuppyError(err) # noqa: B904 - return FunctionType(inputs, output, max_effects_declared=max_effects) + return FunctionType(inputs, output, max_effects_declared=effects) def _parse_self_type(args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx) -> Type: diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index c5e903681..f2a29d1d6 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -423,11 +423,11 @@ class FunctionType(ParametrizedTypeBase): # The None here is to distinguish between explicit and implicit in guppy source code # but is otherwise equivalent to default [Effect.ANY]. Generally use - # `max_effects` instead. + # `effects` instead. max_effects_declared: list[Effect] | None = field(default=None, init=True) @property - def max_effects(self) -> list[Effect]: + def effects(self) -> list[Effect]: return ( self.max_effects_declared if self.max_effects_declared is not None @@ -524,7 +524,7 @@ def _to_hugr_function_type(self, ctx: ToHugrContext) -> ht.FunctionType: The resulting `FunctionType` can then be embedded into a Hugr `Type` or a Hugr `PolyFuncType`. """ - # At some point we may want to represent the max_effects as input and + # At some point we may want to represent the effects as input and # perhaps output "token" types in Hugr, but for now we will use Order edges. ins = [ inp.ty.to_hugr(ctx) @@ -625,12 +625,12 @@ def with_unitary_flags(self, flags: UnitaryFlags) -> "FunctionType": ) def with_effects(self, max_effects_declared: list[Effect] | None) -> "FunctionType": - """Returns a copy of this function type with the specified max_effects.""" + """Returns a copy of this function type with the specified effects.""" # N.B. we can't use `dataclasses.replace` here since `FunctionType` has a custom # constructor if self.max_effects_declared is not None: raise InternalGuppyError( - "Tried to set max_effects on a FunctionType that already has them" + "Tried to set effects on a FunctionType that already has them" ) return FunctionType( self.inputs, @@ -912,7 +912,7 @@ def unify(s: Type | Const, t: Type | Const, subst: "Subst | None") -> "Subst | N case FunctionType() as s, FunctionType() as t if s.params == t.params: if len(s.inputs) != len(t.inputs): return None - if s.max_effects != t.max_effects: + if s.effects != t.effects: # There are no "effect variables" yet, and we enforce exact matching # (invariance) as covariance will become difficult when we replace Order # edges with explicit tokens. (Requiring runtime closures or codegen for diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 7bdbe832f..647b424a9 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -121,7 +121,7 @@ class GuppyKwargs(TypedDict, total=False): power: bool max_qubits: int link_name: str - max_effects: list[Effect] + effects: list[Effect] class GuppyStructKwargs(TypedDict, total=False): @@ -169,7 +169,7 @@ def decorator( unitary_flags=parsed.flags, metadata=parsed.metadata, link_name=parsed.link_name, - max_effects=parsed.max_effects, + effects=parsed.effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -215,7 +215,7 @@ def decorator( f, unitary_flags=parsed.flags, metadata=parsed.metadata, - max_effects=parsed.max_effects, + effects=parsed.effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -480,7 +480,7 @@ def decorator( unitary_flags=parsed.flags, link_name=parsed.link_name, metadata=parsed.metadata, - max_effects=parsed.max_effects, + effects=parsed.effects, ) DEF_STORE.register_def(defn, get_calling_frame()) return GuppyFunctionDefinition(defn) @@ -853,7 +853,7 @@ class ParsedGuppyKwargs(NamedTuple): metadata: FunctionMetadata # The empty list means no effects, whereas None means unspecified - i.e. assume all # effects are possible until we can analyse the call-graph to calculate exactly. - max_effects: list[_Effect] | None + effects: list[_Effect] | None link_name: str | None @@ -877,8 +877,8 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: metadata.set_max_qubits(kwargs.pop("max_qubits")) link_name = kwargs.pop("link_name", None) - max_effects_input = kwargs.pop("max_effects", None) - max_effects = ( + max_effects_input = kwargs.pop("effects", None) + effects = ( None if max_effects_input is None else [effect.to_internal() for effect in max_effects_input] @@ -892,7 +892,7 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: flags=flags, metadata=metadata, link_name=link_name, - max_effects=max_effects, + effects=effects, ) diff --git a/guppylang/src/guppylang/std/angles.py b/guppylang/src/guppylang/std/angles.py index b3fb7e4c2..4b4fb8f5c 100644 --- a/guppylang/src/guppylang/std/angles.py +++ b/guppylang/src/guppylang/std/angles.py @@ -31,47 +31,47 @@ class angle: halfturns: float - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __add__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns + other.halfturns) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __sub__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns - other.halfturns) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __mul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __rmul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __truediv__(self: "angle", other: float) -> "angle": return angle(self.halfturns / other) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __rtruediv__(self: "angle", other: float) -> "angle": return angle(other / self.halfturns) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __neg__(self: "angle") -> "angle": return angle(-self.halfturns) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __float__(self: "angle") -> float: return self.halfturns * py(math.pi) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __eq__(self: "angle", other: "angle") -> bool: return self.halfturns == other.halfturns diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 26585f5b3..7b7ebc263 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -27,92 +27,92 @@ class nat: """A 64-bit unsigned integer.""" - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __abs__(self: nat) -> nat: ... - @hugr_op(int_op("iadd"), max_effects=[]) + @hugr_op(int_op("iadd"), effects=[]) def __add__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("iand"), max_effects=[]) + @hugr_op(int_op("iand"), effects=[]) def __and__(self: nat, other: nat) -> nat: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __bool__(self: nat) -> bool: return self != 0 - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __ceil__(self: nat) -> nat: ... # Panics if other == 0 - @hugr_op(int_op("idivmod_u", n_vars=2), max_effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_u", n_vars=2), effects=[Effect.ANY]) def __divmod__(self: nat, other: nat) -> tuple[nat, nat]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) def __eq__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), max_effects=[]) + @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) def __float__(self: nat) -> float: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __floor__(self: nat) -> nat: ... # Panics if other == 0 - @hugr_op(int_op("idiv_u"), max_effects=[Effect.ANY]) + @hugr_op(int_op("idiv_u"), effects=[Effect.ANY]) def __floordiv__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ige_u")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_u")), effects=[]) def __ge__(self: nat, other: nat) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_u")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_u")), effects=[]) def __gt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("iu_to_s"), max_effects=[]) + @hugr_op(int_op("iu_to_s"), effects=[]) def __int__(self: nat) -> int: ... - @hugr_op(int_op("inot"), max_effects=[]) + @hugr_op(int_op("inot"), effects=[]) def __invert__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ile_u")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_u")), effects=[]) def __le__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("ishl"), max_effects=[]) + @hugr_op(int_op("ishl"), effects=[]) def __lshift__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ilt_u")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ilt_u")), effects=[]) def __lt__(self: nat, other: nat) -> bool: ... # Panics if other == 0 - @hugr_op(int_op("imod_u"), max_effects=[Effect.ANY]) + @hugr_op(int_op("imod_u"), effects=[Effect.ANY]) def __mod__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("imul"), max_effects=[]) + @hugr_op(int_op("imul"), effects=[]) def __mul__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __nat__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) def __ne__(self: nat, other: nat) -> bool: ... @custom_function( - checker=DunderChecker("__nat__"), higher_order_value=False, max_effects=[] + checker=DunderChecker("__nat__"), higher_order_value=False, effects=[] ) def __new__(x): ... - @hugr_op(int_op("ior"), max_effects=[]) + @hugr_op(int_op("ior"), effects=[]) def __or__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: nat) -> nat: ... - @hugr_op(int_op("ipow"), max_effects=[]) + @hugr_op(int_op("ipow"), effects=[]) def __pow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rand__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) @@ -121,40 +121,40 @@ def __rdivmod__(self: nat, other: nat) -> tuple[nat, nat]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rlshift__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __ror__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __round__(self: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rpow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rrshift__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("ishr"), max_effects=[]) + @hugr_op(int_op("ishr"), effects=[]) def __rshift__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rtruediv__(self: nat, other: nat) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rxor__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("isub"), max_effects=[]) + @hugr_op(int_op("isub"), effects=[]) def __sub__(self: nat, other: nat) -> nat: ... @guppy @@ -162,10 +162,10 @@ def __sub__(self: nat, other: nat) -> nat: ... def __truediv__(self: nat, other: nat) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __trunc__(self: nat) -> nat: ... - @hugr_op(int_op("ixor"), max_effects=[]) + @hugr_op(int_op("ixor"), effects=[]) def __xor__(self: nat, other: nat) -> nat: ... @@ -175,54 +175,54 @@ class int: @hugr_op( int_op("iabs"), # TODO: Maybe wrong? (signed vs unsigned!) - max_effects=[], + effects=[], ) def __abs__(self: int) -> int: ... - @hugr_op(int_op("iadd"), max_effects=[]) + @hugr_op(int_op("iadd"), effects=[]) def __add__(self: int, other: int) -> int: ... - @hugr_op(int_op("iand"), max_effects=[]) + @hugr_op(int_op("iand"), effects=[]) def __and__(self: int, other: int) -> int: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __bool__(self: int) -> bool: return self != 0 - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __ceil__(self: int) -> int: ... # Panics if other == 0 - @hugr_op(int_op("idivmod_s"), max_effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_s"), effects=[Effect.ANY]) def __divmod__(self: int, other: int) -> tuple[int, int]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) def __eq__(self: int, other: int) -> bool: ... - @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), max_effects=[]) + @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) def __float__(self: int) -> float: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __floor__(self: int) -> int: ... # Panics if other == 0 - @hugr_op(int_op("idiv_s"), max_effects=[Effect.ANY]) + @hugr_op(int_op("idiv_s"), effects=[Effect.ANY]) def __floordiv__(self: int, other: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ige_s")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_s")), effects=[]) def __ge__(self: int, other: int) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_s")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_s")), effects=[]) def __gt__(self: int, other: int) -> bool: ... @custom_function(NoopCompiler()) def __int__(self: int) -> int: ... - @hugr_op(int_op("inot"), max_effects=[]) + @hugr_op(int_op("inot"), effects=[]) def __invert__(self: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ile_s")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_s")), effects=[]) def __le__(self: int, other: int) -> bool: ... @hugr_op(int_op("ishl")) # TODO: RHS is unsigned @@ -231,30 +231,30 @@ def __lshift__(self: int, other: int) -> int: ... @custom_function(BoolOpCompiler(int_op("ilt_s"))) def __lt__(self: int, other: int) -> bool: ... - @hugr_op(int_op("imod_s"), max_effects=[]) + @hugr_op(int_op("imod_s"), effects=[]) def __mod__(self: int, other: int) -> int: ... - @hugr_op(int_op("imul"), max_effects=[]) + @hugr_op(int_op("imul"), effects=[]) def __mul__(self: int, other: int) -> int: ... - @hugr_op(int_op("is_to_u"), max_effects=[]) + @hugr_op(int_op("is_to_u"), effects=[]) def __nat__(self: int) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), max_effects=[]) + @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) def __ne__(self: int, other: int) -> bool: ... - @hugr_op(int_op("ineg"), max_effects=[]) + @hugr_op(int_op("ineg"), effects=[]) def __neg__(self: int) -> int: ... @custom_function( - checker=DunderChecker("__int__"), higher_order_value=False, max_effects=[] + checker=DunderChecker("__int__"), higher_order_value=False, effects=[] ) def __new__(x): ... - @hugr_op(int_op("ior"), max_effects=[]) + @hugr_op(int_op("ior"), effects=[]) def __or__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: int) -> int: ... @guppy @@ -267,13 +267,13 @@ def __pow__(self: int, exponent: int) -> int: ) return self.__pow_impl(exponent) - @hugr_op(int_op("ipow"), max_effects=[]) # Exponent is treated as unsigned + @hugr_op(int_op("ipow"), effects=[]) # Exponent is treated as unsigned def __pow_impl(self: int, exponent: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rand__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -284,7 +284,7 @@ def __rfloordiv__(self: int, other: int) -> int: ... @custom_function( checker=ReversingChecker(), # TODO: RHS is unsigned - max_effects=[], + effects=[], ) def __rlshift__(self: int, other: int) -> int: ... @@ -292,13 +292,13 @@ def __rlshift__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __ror__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __round__(self: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -309,31 +309,31 @@ def __rrshift__(self: int, other: int) -> int: ... @hugr_op( int_op("ishr"), # TODO: RHS is unsigned - max_effects=[], + effects=[], ) def __rshift__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rtruediv__(self: int, other: int) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rxor__(self: int, other: int) -> int: ... - @hugr_op(int_op("isub"), max_effects=[]) + @hugr_op(int_op("isub"), effects=[]) def __sub__(self: int, other: int) -> int: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __truediv__(self: int, other: int) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __trunc__(self: int) -> int: ... - @hugr_op(int_op("ixor"), max_effects=[]) + @hugr_op(int_op("ixor"), effects=[]) def __xor__(self: int, other: int) -> int: ... @@ -341,43 +341,43 @@ def __xor__(self: int, other: int) -> int: ... class float: """An IEEE754 double-precision floating point value.""" - @hugr_op(float_op("fabs"), max_effects=[]) + @hugr_op(float_op("fabs"), effects=[]) def __abs__(self: float) -> float: ... - @hugr_op(float_op("fadd"), max_effects=[]) + @hugr_op(float_op("fadd"), effects=[]) def __add__(self: float, other: float) -> float: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __bool__(self: float) -> bool: return self != 0.0 - @hugr_op(float_op("fceil"), max_effects=[]) + @hugr_op(float_op("fceil"), effects=[]) def __ceil__(self: float) -> float: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __divmod__(self: float, other: float) -> tuple[float, float]: return self // other, self.__mod__(other) - @custom_function(BoolOpCompiler(float_op("feq")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("feq")), effects=[]) def __eq__(self: float, other: float) -> bool: ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __float__(self: float) -> float: ... - @hugr_op(float_op("ffloor"), max_effects=[]) + @hugr_op(float_op("ffloor"), effects=[]) def __floor__(self: float) -> float: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __floordiv__(self: float, other: float) -> float: return (self / other).__floor__() - @custom_function(BoolOpCompiler(float_op("fge")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("fge")), effects=[]) def __ge__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("fgt")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("fgt")), effects=[]) def __gt__(self: float, other: float) -> bool: ... @custom_function( @@ -385,22 +385,22 @@ def __gt__(self: float, other: float) -> bool: ... # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_s", hugr.std.int.CONVERSIONS_EXTENSION), ), - max_effects=[], + effects=[], ) def __int__(self: float) -> int: ... - @custom_function(BoolOpCompiler(float_op("fle")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("fle")), effects=[]) def __le__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("flt")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("flt")), effects=[]) def __lt__(self: float, other: float) -> bool: ... - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __mod__(self: float, other: float) -> float: return self - (self // other) * other - @hugr_op(float_op("fmul"), max_effects=[]) + @hugr_op(float_op("fmul"), effects=[]) def __mul__(self: float, other: float) -> float: ... @custom_function( @@ -408,63 +408,61 @@ def __mul__(self: float, other: float) -> float: ... # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_u", hugr.std.int.CONVERSIONS_EXTENSION), ), - max_effects=[], + effects=[], ) def __nat__(self: float) -> nat: ... - @custom_function(BoolOpCompiler(float_op("fne")), max_effects=[]) + @custom_function(BoolOpCompiler(float_op("fne")), effects=[]) def __ne__(self: float, other: float) -> bool: ... - @hugr_op(float_op("fneg"), max_effects=[]) + @hugr_op(float_op("fneg"), effects=[]) def __neg__(self: float) -> float: ... @custom_function( - checker=DunderChecker("__float__"), higher_order_value=False, max_effects=[] + checker=DunderChecker("__float__"), higher_order_value=False, effects=[] ) def __new__(x): ... - @custom_function(NoopCompiler(), max_effects=[]) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: float) -> float: ... - @hugr_op(float_op("fpow"), max_effects=[]) # TODO + @hugr_op(float_op("fpow"), effects=[]) # TODO def __pow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rdivmod__(self: float, other: float) -> tuple[float, float]: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rfloordiv__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmod__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: float, other: float) -> float: ... - @hugr_op(float_op("fround"), max_effects=[]) # TODO + @hugr_op(float_op("fround"), effects=[]) # TODO def __round__(self: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rpow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), max_effects=[]) + @custom_function(checker=ReversingChecker(), effects=[]) def __rtruediv__(self: float, other: float) -> float: ... - @hugr_op(float_op("fsub"), max_effects=[]) + @hugr_op(float_op("fsub"), effects=[]) def __sub__(self: float, other: float) -> float: ... - @hugr_op(float_op("fdiv"), max_effects=[]) + @hugr_op(float_op("fdiv"), effects=[]) def __truediv__(self: float, other: float) -> float: ... - @hugr_op( - unsupported_op("trunc_s"), max_effects=[] - ) # TODO `trunc_s` returns an option + @hugr_op(unsupported_op("trunc_s"), effects=[]) # TODO `trunc_s` returns an option def __trunc__(self: float) -> float: ... diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index 7529d8901..e6c0d23f0 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -257,14 +257,14 @@ class Measurement: before being used.""" # We do *not* model the pipeline as a side-effect - @custom_function(compiler=ReadFutureBoolCompiler(), max_effects=[]) + @custom_function(compiler=ReadFutureBoolCompiler(), effects=[]) @no_type_check def read(self: "Measurement" @ owned) -> bool: """Read the measurement result, consuming it. Blocks until the result is available if the measurement hasn't been performed yet since being requested. """ - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def __consume_as_bool__(self: "Measurement" @ owned) -> bool: return self.read() diff --git a/guppylang/src/guppylang/std/quantum/__init__.py b/guppylang/src/guppylang/std/quantum/__init__.py index 225dc5a35..0c78b305b 100644 --- a/guppylang/src/guppylang/std/quantum/__init__.py +++ b/guppylang/src/guppylang/std/quantum/__init__.py @@ -31,7 +31,7 @@ def __new__() -> "qubit": ... def measure(self: "qubit" @ owned) -> bool: return measure(self) - @guppy(max_effects=[]) + @guppy(effects=[]) @no_type_check def project_z(self: "qubit") -> bool: return project_z(self) @@ -49,7 +49,7 @@ def maybe_qubit() -> Option[qubit]: if allocation succeeds or `nothing` if it fails.""" -@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def h(q: qubit) -> None: r"""Hadamard gate command @@ -63,7 +63,7 @@ def h(q: qubit) -> None: """ -@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cz(control: qubit, target: qubit) -> None: r"""Controlled-Z gate command. @@ -83,7 +83,7 @@ def cz(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cy(control: qubit, target: qubit) -> None: r"""Controlled-Y gate command. @@ -103,7 +103,7 @@ def cy(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cx(control: qubit, target: qubit) -> None: r"""Controlled-X gate command. @@ -123,7 +123,7 @@ def cx(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def t(q: qubit) -> None: r"""T gate. @@ -138,7 +138,7 @@ def t(q: qubit) -> None: """ -@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def s(q: qubit) -> None: r"""S gate. @@ -153,7 +153,7 @@ def s(q: qubit) -> None: """ -@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def v(q: qubit) -> None: r"""V gate. @@ -168,7 +168,7 @@ def v(q: qubit) -> None: """ -@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def x(q: qubit) -> None: r"""X gate. @@ -183,7 +183,7 @@ def x(q: qubit) -> None: """ -@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def y(q: qubit) -> None: r"""Y gate. @@ -198,7 +198,7 @@ def y(q: qubit) -> None: """ -@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def z(q: qubit) -> None: r"""Z gate. @@ -213,7 +213,7 @@ def z(q: qubit) -> None: """ -@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def tdg(q: qubit) -> None: r"""Tdg gate. @@ -228,7 +228,7 @@ def tdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def sdg(q: qubit) -> None: r"""Sdg gate. @@ -243,7 +243,7 @@ def sdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def vdg(q: qubit) -> None: r"""Vdg gate. @@ -258,9 +258,7 @@ def vdg(q: qubit) -> None: """ -@custom_function( - RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] -) +@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def rz(q: qubit, angle: angle) -> None: r"""Rz gate. @@ -276,9 +274,7 @@ def rz(q: qubit, angle: angle) -> None: """ -@custom_function( - RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] -) +@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def rx(q: qubit, angle: angle) -> None: r"""Rx gate. @@ -293,9 +289,7 @@ def rx(q: qubit, angle: angle) -> None: """ -@custom_function( - RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] -) +@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def ry(q: qubit, angle: angle) -> None: r"""Ry gate. @@ -311,7 +305,7 @@ def ry(q: qubit, angle: angle) -> None: @custom_function( - RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, max_effects=[] + RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, effects=[] ) @no_type_check def crz(control: qubit, target: qubit, angle: angle) -> None: @@ -332,7 +326,7 @@ def crz(control: qubit, target: qubit, angle: angle) -> None: """ -@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, max_effects=[]) +@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: r"""A Toffoli gate command. Also sometimes known as a CCX gate. @@ -356,7 +350,7 @@ def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: """ -@custom_function(InoutMeasureCompiler(), max_effects=[]) +@custom_function(InoutMeasureCompiler(), effects=[]) @no_type_check def project_z(q: qubit) -> bool: """Project a single qubit into the Z-basis (a non-destructive measurement).""" @@ -374,7 +368,7 @@ def measure(q: qubit @ owned) -> bool: """Measure a single qubit destructively.""" -@hugr_op(quantum_op("Reset"), max_effects=[]) +@hugr_op(quantum_op("Reset"), effects=[]) @no_type_check def reset(q: qubit) -> None: """Reset a single qubit to the :math:`|0\rangle` state.""" @@ -403,7 +397,7 @@ def discard_array(qubits: array[qubit, N] @ owned) -> None: # -------NON-PRIMITIVE------- -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def ch(control: qubit, target: qubit) -> None: r"""Controlled-H gate command. diff --git a/guppylang/src/guppylang/std/quantum/functional.py b/guppylang/src/guppylang/std/quantum/functional.py index aeb0b524f..756348b1e 100644 --- a/guppylang/src/guppylang/std/quantum/functional.py +++ b/guppylang/src/guppylang/std/quantum/functional.py @@ -15,7 +15,7 @@ from guppylang.std.quantum import qubit -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def h(q: qubit @ owned) -> qubit: """Functional Hadamard gate command.""" @@ -23,7 +23,7 @@ def h(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CZ gate command.""" @@ -31,7 +31,7 @@ def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CX gate command.""" @@ -39,7 +39,7 @@ def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CY gate command.""" @@ -47,7 +47,7 @@ def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def t(q: qubit @ owned) -> qubit: """Functional T gate command.""" @@ -55,7 +55,7 @@ def t(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def s(q: qubit @ owned) -> qubit: """Functional S gate command.""" @@ -63,7 +63,7 @@ def s(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def v(q: qubit @ owned) -> qubit: """Functional V gate command.""" @@ -71,7 +71,7 @@ def v(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def x(q: qubit @ owned) -> qubit: """Functional X gate command.""" @@ -79,7 +79,7 @@ def x(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def y(q: qubit @ owned) -> qubit: """Functional Y gate command.""" @@ -87,7 +87,7 @@ def y(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def z(q: qubit @ owned) -> qubit: """Functional Z gate command.""" @@ -95,7 +95,7 @@ def z(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def tdg(q: qubit @ owned) -> qubit: """Functional Tdg gate command.""" @@ -103,7 +103,7 @@ def tdg(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def sdg(q: qubit @ owned) -> qubit: """Functional Sdg gate command.""" @@ -111,7 +111,7 @@ def sdg(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def vdg(q: qubit @ owned) -> qubit: """Functional Vdg gate command.""" @@ -119,7 +119,7 @@ def vdg(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def rz(q: qubit @ owned, angle: angle) -> qubit: """Functional Rz gate command.""" @@ -127,7 +127,7 @@ def rz(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def rx(q: qubit @ owned, angle: angle) -> qubit: """Functional Rx gate command.""" @@ -135,7 +135,7 @@ def rx(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def ry(q: qubit @ owned, angle: angle) -> qubit: """Functional Ry gate command.""" @@ -143,7 +143,7 @@ def ry(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def crz( control: qubit @ owned, target: qubit @ owned, angle: angle @@ -153,7 +153,7 @@ def crz( return control, target -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def toffoli( control1: qubit @ owned, control2: qubit @ owned, target: qubit @ owned @@ -163,7 +163,7 @@ def toffoli( return control1, control2, target -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def reset(q: qubit @ owned) -> qubit: """Functional Reset command.""" @@ -171,7 +171,7 @@ def reset(q: qubit @ owned) -> qubit: return q -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def project_z(q: qubit @ owned) -> tuple[qubit, bool]: """Functional project_z command.""" @@ -182,7 +182,7 @@ def project_z(q: qubit @ owned) -> tuple[qubit, bool]: # -------NON-PRIMITIVE------- -@guppy(max_effects=[]) +@guppy(effects=[]) @no_type_check def ch(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional Controlled-H gate command.""" diff --git a/tests/error/effects_errors/overload.err b/tests/error/effects_errors/overload.err index 425e1631d..529e4f994 100644 --- a/tests/error/effects_errors/overload.err +++ b/tests/error/effects_errors/overload.err @@ -1,6 +1,6 @@ Error: Invalid call of overloaded function (at $FILE:24:11) | -22 | @guppy(max_effects=[]) +22 | @guppy(effects=[]) 23 | def bad_pure_func(x: float) -> float: 24 | return only_pure_for_int(x) | ^^^^^^^^^^^^^^^^^^^^ No variant of overloaded function `only_pure_for_int` takes diff --git a/tests/error/effects_errors/overload.py b/tests/error/effects_errors/overload.py index 6fbe03729..4cbb459ae 100644 --- a/tests/error/effects_errors/overload.py +++ b/tests/error/effects_errors/overload.py @@ -5,13 +5,13 @@ @guppy.declare def variant1(x : T) -> T: ... -@guppy.declare(max_effects=[]) +@guppy.declare(effects=[]) def variant2(x : int) -> int: ... @guppy.overload(variant1, variant2) def only_pure_for_int(): ... -@guppy(max_effects=[]) +@guppy(effects=[]) def pure_func(x: int) -> int: return only_pure_for_int(x + 1) @@ -19,7 +19,7 @@ def pure_func(x: int) -> int: def impure_func(x: float) -> float: return only_pure_for_int(x + 1.0) -@guppy(max_effects=[]) +@guppy(effects=[]) def bad_pure_func(x: float) -> float: return only_pure_for_int(x) diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index dc2d0dd3a..724910034 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:5:10) | -3 | @guppy(max_effects=[]) +3 | @guppy(effects=[]) 4 | def main(impure_f: Callable[[int], int, [ANY]]) -> int: 5 | return impure_f(5) | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:5:10) Note: | 2 | -3 | @guppy(max_effects=[]) +3 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.py b/tests/error/effects_errors/pure_calls_explicit_callable.py index 4e8ab39a6..1f23bf5be 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.py +++ b/tests/error/effects_errors/pure_calls_explicit_callable.py @@ -1,6 +1,6 @@ from guppylang.decorator import guppy -@guppy(max_effects=[]) +@guppy(effects=[]) def main(impure_f: Callable[[int], int, [ANY]]) -> int: return impure_f(5) diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err index b905c75ba..26959090d 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.err +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:8:10) | -6 | @guppy(max_effects=[]) +6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:8:10) Note: | 5 | -6 | @guppy(max_effects=[]) +6 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.py b/tests/error/effects_errors/pure_calls_explicit_decl.py index a614b60ed..29facc00f 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.py +++ b/tests/error/effects_errors/pure_calls_explicit_decl.py @@ -1,9 +1,9 @@ from guppylang.decorator import guppy, Effect -@guppy.declare(max_effects=[Effect.ANY]) +@guppy.declare(effects=[Effect.ANY]) def impure_func(x: int) -> int: ... -@guppy(max_effects=[]) +@guppy(effects=[]) def main() -> int: return impure_func(5) diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err index 617b10b0f..012088769 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.err +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:9:10) | -7 | @guppy(max_effects=[]) +7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:9:10) Note: | 6 | -7 | @guppy(max_effects=[]) +7 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_def.py b/tests/error/effects_errors/pure_calls_explicit_def.py index c94731455..97478da44 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.py +++ b/tests/error/effects_errors/pure_calls_explicit_def.py @@ -1,10 +1,10 @@ from guppylang.decorator import guppy, Effect -@guppy(max_effects=[Effect.ANY]) +@guppy(effects=[Effect.ANY]) def impure_func(x: int) -> int: return x + 1 -@guppy(max_effects=[]) +@guppy(effects=[]) def main() -> int: return impure_func(5) diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 02b29b21b..e23d3022d 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:5:10) | -3 | @guppy(max_effects=[]) +3 | @guppy(effects=[]) 4 | def main(impure_f: Callable[[int], int]) -> int: 5 | return impure_f(5) | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:5:10) Note: | 2 | -3 | @guppy(max_effects=[]) +3 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable.py b/tests/error/effects_errors/pure_calls_impure_callable.py index ae1ef9fd5..9629bcbf6 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.py +++ b/tests/error/effects_errors/pure_calls_impure_callable.py @@ -1,6 +1,6 @@ from guppylang.decorator import guppy -@guppy(max_effects=[]) +@guppy(effects=[]) def main(impure_f: Callable[[int], int]) -> int: return impure_f(5) diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 91f5df0ca..1b2c8dde6 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:8:10) | -6 | @guppy(max_effects=[]) +6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:8:10) Note: | 5 | -6 | @guppy(max_effects=[]) +6 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.py b/tests/error/effects_errors/pure_calls_impure_decl.py index 6ca19211d..2bda26cc7 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.py +++ b/tests/error/effects_errors/pure_calls_impure_decl.py @@ -3,7 +3,7 @@ @guppy.declare def impure_func(x: int) -> int: ... -@guppy(max_effects=[]) +@guppy(effects=[]) def main() -> int: return impure_func(5) diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 5173516f5..00dd72d65 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -1,6 +1,6 @@ Error: Too many effects (at $FILE:9:10) | -7 | @guppy(max_effects=[]) +7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed @@ -9,7 +9,7 @@ Error: Too many effects (at $FILE:9:10) Note: | 6 | -7 | @guppy(max_effects=[]) +7 | @guppy(effects=[]) | --------------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.py b/tests/error/effects_errors/pure_calls_impure_def.py index abaaaeb53..3bbef1121 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.py +++ b/tests/error/effects_errors/pure_calls_impure_def.py @@ -4,7 +4,7 @@ def impure_func(x: int) -> int: return x + 1 -@guppy(max_effects=[]) +@guppy(effects=[]) def main() -> int: return impure_func(5) diff --git a/tests/error/effects_errors/pure_result.err b/tests/error/effects_errors/pure_result.err index baec08100..fdacbc842 100644 --- a/tests/error/effects_errors/pure_result.err +++ b/tests/error/effects_errors/pure_result.err @@ -1,6 +1,6 @@ Error: Invalid call of overloaded function (at $FILE:6:10) | -4 | @guppy(max_effects=[]) +4 | @guppy(effects=[]) 5 | def main() -> int: 6 | result("foo", True) | ^^^^^^^^^^^ No variant of overloaded function `result` takes arguments diff --git a/tests/error/effects_errors/pure_result.py b/tests/error/effects_errors/pure_result.py index daf1827b8..3d535bafd 100644 --- a/tests/error/effects_errors/pure_result.py +++ b/tests/error/effects_errors/pure_result.py @@ -1,7 +1,7 @@ from guppylang.decorator import guppy from guppylang.std.builtins import result -@guppy(max_effects=[]) +@guppy(effects=[]) def main() -> int: result("foo", True) return 3 diff --git a/tests/error/effects_errors/return_explicit_callable.py b/tests/error/effects_errors/return_explicit_callable.py index 55ad916c7..9319d045b 100644 --- a/tests/error/effects_errors/return_explicit_callable.py +++ b/tests/error/effects_errors/return_explicit_callable.py @@ -1,6 +1,6 @@ from guppylang.decorator import guppy, Effect -@guppy(max_effects=[Effect.ANY]) +@guppy(effects=[Effect.ANY]) def impure_func(x: int) -> int: return x + 1 diff --git a/tests/error/effects_errors/return_pure_callable.py b/tests/error/effects_errors/return_pure_callable.py index f2678d344..dcab1cf69 100644 --- a/tests/error/effects_errors/return_pure_callable.py +++ b/tests/error/effects_errors/return_pure_callable.py @@ -1,6 +1,6 @@ from guppylang.decorator import guppy -@guppy(max_effects=[]) +@guppy(effects=[]) def pure_func(x: int) -> int: return x + 1 diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index b4f436008..ea33fc571 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -1,4 +1,4 @@ -"""Tests of max_effects annotation.""" +"""Tests of effects annotation.""" import pytest from collections.abc import Callable @@ -8,7 +8,7 @@ def test_pure_decl_from_impure(validate): - @guppy.declare(max_effects=[]) + @guppy.declare(effects=[]) def pure_func(x: int) -> int: ... @guppy @@ -19,10 +19,10 @@ def impure_func(x: int) -> int: def test_pure_decl_from_explicit_impure(validate): - @guppy.declare(max_effects=[]) + @guppy.declare(effects=[]) def pure_func(x: int) -> int: ... - @guppy(max_effects=[Effect.ANY]) + @guppy(effects=[Effect.ANY]) def impure_func(x: int) -> int: return pure_func(x) + 1 @@ -30,10 +30,10 @@ def impure_func(x: int) -> int: def test_pure_decl_from_pure(validate): - @guppy.declare(max_effects=[]) + @guppy.declare(effects=[]) def pure_func1(x: int) -> int: ... - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func2(x: int) -> int: return pure_func1(x) + 2 @@ -43,9 +43,9 @@ def pure_func2(x: int) -> int: @pytest.mark.parametrize( ("caller", "callee"), [ - ({"max_effects": [Effect.ANY]}, {}), - ({}, {"max_effects": [Effect.ANY]}), - ({"max_effects": [Effect.ANY]}, {"max_effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {}), + ({}, {"effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {"effects": [Effect.ANY]}), ], ) def test_impure_decl_explicit(caller, callee, validate): @@ -60,7 +60,7 @@ def impure_func2(x: int) -> int: def test_pure_from_impure(validate): - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func(x: int) -> int: return x + 1 @@ -72,11 +72,11 @@ def normal_func(x: int) -> int: def test_pure_from_explicit_impure(validate): - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func(x: int) -> int: return x + 1 - @guppy(max_effects=[Effect.ANY]) + @guppy(effects=[Effect.ANY]) def normal_func(x: int) -> int: return pure_func(x) + 2 @@ -86,9 +86,9 @@ def normal_func(x: int) -> int: @pytest.mark.parametrize( ("caller", "callee"), [ - ({"max_effects": [Effect.ANY]}, {}), - ({}, {"max_effects": [Effect.ANY]}), - ({"max_effects": [Effect.ANY]}, {"max_effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {}), + ({}, {"effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {"effects": [Effect.ANY]}), ], ) def test_impure_explicit(caller, callee, validate): @@ -105,11 +105,11 @@ def impure_func2(x: int) -> int: def test_pure_from_pure(validate): - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func1(x: int) -> int: return x + 1 - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func2(x: int) -> int: return pure_func1(pure_func1(x)) + 1 @@ -125,7 +125,7 @@ def impure_func(pure_f: Callable[[int], int, []]) -> int: def test_pure_callable_from_pure(validate): - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func(pure_f: Callable[[int], int, []]) -> int: return pure_f(5) + 1 @@ -133,7 +133,7 @@ def pure_func(pure_f: Callable[[int], int, []]) -> int: def test_pure_callable_from_impure_explicit(validate): - @guppy(max_effects=[Effect.ANY]) + @guppy(effects=[Effect.ANY]) def impure_func(pure_f: Callable[[int], int, []]) -> int: return pure_f(5) + 1 @@ -145,7 +145,7 @@ def test_return_callable1(validate): def impure_func(x: int) -> int: return x + 1 - @guppy(max_effects=[]) + @guppy(effects=[]) def higher_order() -> Callable[[int], int, [ANY]]: # noqa: F821 return impure_func @@ -153,11 +153,11 @@ def higher_order() -> Callable[[int], int, [ANY]]: # noqa: F821 def test_return_callable2(validate): - @guppy(max_effects=[Effect.ANY]) + @guppy(effects=[Effect.ANY]) def explicit_impure_func(x: int) -> int: return x + 1 - @guppy(max_effects=[]) + @guppy(effects=[]) def higher_order() -> Callable[[int], int]: return explicit_impure_func diff --git a/tests/integration/test_higher_order.py b/tests/integration/test_higher_order.py index 9445399b9..a5aaa8b72 100644 --- a/tests/integration/test_higher_order.py +++ b/tests/integration/test_higher_order.py @@ -182,7 +182,7 @@ def fac(x: int) -> int: # https://github.com/Quantinuum/guppylang/issues/1760 # but presently exists to show the part of the test that *does* work def test_higher_order_effects1(validate): - @guppy(max_effects=[Effect.ANY]) + @guppy(effects=[Effect.ANY]) def impure_func(x: int) -> int: return x + 1 @@ -200,7 +200,7 @@ def main() -> int: @pytest.mark.xfail(reason="Pending https://github.com/Quantinuum/guppylang/issues/1760") def test_higher_order_effects2(validate): - @guppy(max_effects=[]) + @guppy(effects=[]) def pure_func(x: int) -> int: return x + 1 @@ -210,7 +210,7 @@ def pure_func(x: int) -> int: def higher_order(f: Callable[[int], int], x: int) -> int: return f(x) - @guppy(max_effects=[]) + @guppy(effects=[]) def main() -> int: return higher_order(pure_func, 5) From 442033eceea9e034497860d736ef4c2f129b32b3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 15:49:42 +0100 Subject: [PATCH 46/81] max_effects_declared -> declared_effects --- .../checker/func_checker.py | 8 +++---- .../guppylang_internals/definition/struct.py | 2 +- .../src/guppylang_internals/tys/parsing.py | 2 +- .../src/guppylang_internals/tys/printing.py | 4 ++-- .../src/guppylang_internals/tys/ty.py | 22 +++++++++---------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index c3982aff5..83472a4d6 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -160,16 +160,16 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } - if ty.max_effects_declared is None: + if ty.declared_effects is None: max_effects_from = None else: dec = _find_guppy_decorator(func_def.decorator_list) - if dec is None and ty.max_effects_declared is not None: + if dec is None and ty.declared_effects is not None: raise InternalGuppyError( - f"Effects limited to {Effect.format_list(ty.max_effects_declared)}" + f"Effects limited to {Effect.format_list(ty.declared_effects)}" " but cannot identify decorator imposing this limit" ) - max_effects_from = (ty.max_effects_declared, dec) + max_effects_from = (ty.declared_effects, dec) return check_cfg( cfg, inputs, diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index 3ac8fb533..ff40842c9 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -225,7 +225,7 @@ def compile(self, args: list[Wire]) -> list[Wire]: defn=self, args=[p.to_bound(i) for i, p in enumerate(self.params)] ), params=self.params, - max_effects_declared=[], + declared_effects=[], ) constructor_def = CustomFunctionDef( id=DefId.fresh(), diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 34775d330..64b095fac 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -304,7 +304,7 @@ def _parse_callable_type( effects.append(Effect.__from_str__(e.id)) except ValueError: raise GuppyError(err) # noqa: B904 - return FunctionType(inputs, output, max_effects_declared=effects) + return FunctionType(inputs, output, declared_effects=effects) def _parse_self_type(args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx) -> Type: diff --git a/guppylang-internals/src/guppylang_internals/tys/printing.py b/guppylang-internals/src/guppylang_internals/tys/printing.py index 68377c186..1525a66ad 100644 --- a/guppylang-internals/src/guppylang_internals/tys/printing.py +++ b/guppylang-internals/src/guppylang_internals/tys/printing.py @@ -98,8 +98,8 @@ def _visit_FunctionType(self, ty: FunctionType, inside_row: bool) -> str: output = self._visit(ty.output, True) arrow = ( "->" - if ty.max_effects_declared is None - else f"-{Effect.format_list(ty.max_effects_declared)}->" + if ty.declared_effects is None + else f"-{Effect.format_list(ty.declared_effects)}->" ) if ty.parametrized: params = [ diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index f2a29d1d6..0b25b3daa 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -424,14 +424,12 @@ class FunctionType(ParametrizedTypeBase): # The None here is to distinguish between explicit and implicit in guppy source code # but is otherwise equivalent to default [Effect.ANY]. Generally use # `effects` instead. - max_effects_declared: list[Effect] | None = field(default=None, init=True) + declared_effects: list[Effect] | None = field(default=None, init=True) @property def effects(self) -> list[Effect]: return ( - self.max_effects_declared - if self.max_effects_declared is not None - else [Effect.ANY] + self.declared_effects if self.declared_effects is not None else [Effect.ANY] ) def __init__( @@ -441,7 +439,7 @@ def __init__( params: Sequence[Parameter] | None = None, comptime_args: Sequence[ConstArg] | None = None, unitary_flags: UnitaryFlags = UnitaryFlags.NoFlags, - max_effects_declared: list[Effect] | None = None, + declared_effects: list[Effect] | None = None, ) -> None: # We need a custom __init__ to set the args args: list[Argument] = [TypeArg(inp.ty) for inp in inputs] @@ -470,7 +468,7 @@ def __init__( object.__setattr__(self, "output", output) object.__setattr__(self, "params", params) object.__setattr__(self, "unitary_flags", unitary_flags) - object.__setattr__(self, "max_effects_declared", max_effects_declared) + object.__setattr__(self, "declared_effects", declared_effects) @property def parametrized(self) -> bool: @@ -560,7 +558,7 @@ def transform(self, transformer: Transformer) -> "Type": self.params, comptime_args=self.comptime_args, unitary_flags=self.unitary_flags, - max_effects_declared=self.max_effects_declared, + declared_effects=self.declared_effects, ) def instantiate_partial(self, args: "PartialInst") -> "FunctionType": @@ -590,7 +588,7 @@ def instantiate_partial(self, args: "PartialInst") -> "FunctionType": cast("ConstArg", arg.transform(inst)) for arg in self.comptime_args ], unitary_flags=self.unitary_flags, - max_effects_declared=self.max_effects_declared, + declared_effects=self.declared_effects, ) def instantiate(self, args: "Inst") -> "FunctionType": @@ -621,14 +619,14 @@ def with_unitary_flags(self, flags: UnitaryFlags) -> "FunctionType": self.params, self.comptime_args, flags, - max_effects_declared=self.max_effects_declared, + declared_effects=self.declared_effects, ) - def with_effects(self, max_effects_declared: list[Effect] | None) -> "FunctionType": + def with_effects(self, declared_effects: list[Effect] | None) -> "FunctionType": """Returns a copy of this function type with the specified effects.""" # N.B. we can't use `dataclasses.replace` here since `FunctionType` has a custom # constructor - if self.max_effects_declared is not None: + if self.declared_effects is not None: raise InternalGuppyError( "Tried to set effects on a FunctionType that already has them" ) @@ -638,7 +636,7 @@ def with_effects(self, max_effects_declared: list[Effect] | None) -> "FunctionTy self.params, self.comptime_args, self.unitary_flags, - max_effects_declared=max_effects_declared, + declared_effects=declared_effects, ) From 5f22a660bb2f87d740b776cefb122cb235b61587 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 15:52:49 +0100 Subject: [PATCH 47/81] doc comment max_effects_from / declared_effects --- guppylang-internals/src/guppylang_internals/checker/core.py | 3 +++ guppylang-internals/src/guppylang_internals/tys/ty.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index c9c964ae5..3b1bc2989 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -448,6 +448,9 @@ class Context(NamedTuple): 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: tuple[list[Effect], AstNode] | None = None @property diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index 0b25b3daa..f5811248a 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -421,9 +421,9 @@ class FunctionType(ParametrizedTypeBase): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, init=True) - # The None here is to distinguish between explicit and implicit in guppy source code - # but is otherwise equivalent to default [Effect.ANY]. Generally use - # `effects` instead. + """ Effects declared in source code (i.e. as third argument to Callable). + None means there was no declaration, which is equivalent to [Effect.ANY] + except for error reporting. Generally use `effects` instead.""" declared_effects: list[Effect] | None = field(default=None, init=True) @property From 279a4abfaa79ef0198d23aee35dcbac8723a9a89 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 16:01:38 +0100 Subject: [PATCH 48/81] typing_extensions.assert_never --- guppylang/src/guppylang/decorator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 647b424a9..c69772e2a 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -10,7 +10,6 @@ ParamSpec, TypedDict, TypeVar, - assert_never, cast, overload, ) @@ -70,7 +69,7 @@ from hugr import tys as ht from hugr import val as hv from hugr.package import ModulePointer -from typing_extensions import Unpack, dataclass_transform, deprecated +from typing_extensions import Unpack, assert_never, dataclass_transform, deprecated from guppylang.defs import ( GuppyDefinition, From 8b662c5de286aa00f69642f0e4ba6b1f7124a0ed Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 16:11:26 +0100 Subject: [PATCH 49/81] Error messages shortened by rename (!) --- tests/error/effects_errors/pure_calls_explicit_callable.err | 2 +- tests/error/effects_errors/pure_calls_explicit_decl.err | 2 +- tests/error/effects_errors/pure_calls_explicit_def.err | 2 +- tests/error/effects_errors/pure_calls_impure_callable.err | 2 +- tests/error/effects_errors/pure_calls_impure_decl.err | 2 +- tests/error/effects_errors/pure_calls_impure_def.err | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 724910034..808b38e56 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -10,6 +10,6 @@ Note: | 2 | 3 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err index 26959090d..e130c5f68 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.err +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -10,6 +10,6 @@ Note: | 5 | 6 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err index 012088769..a75aeaa1b 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.err +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -10,6 +10,6 @@ Note: | 6 | 7 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index e23d3022d..9f7aecd9f 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -10,6 +10,6 @@ Note: | 2 | 3 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 1b2c8dde6..f3751dad4 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -10,6 +10,6 @@ Note: | 5 | 6 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 00dd72d65..4a7c268d1 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -10,6 +10,6 @@ Note: | 6 | 7 | @guppy(effects=[]) - | --------------------- Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error From 06533b2d15c33a86050e84e5bf45c218445bb1c7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 16:12:15 +0100 Subject: [PATCH 50/81] fix test_struct load_constructor --- tests/integration/tracing/test_struct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index f9cb545df..c3d4cd561 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -114,7 +114,7 @@ class S: x: int @guppy.comptime - def test() -> Callable[[int], S]: + def test() -> Callable[[int], S, []]: return S validate(test.compile_function()) From dcfe743c5b0a26c4a3b0073b7985d1fe62a9ce24 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 16:25:38 +0100 Subject: [PATCH 51/81] Add collections.abc import for 3.10 --- tests/error/effects_errors/pure_calls_explicit_callable.py | 2 ++ tests/error/effects_errors/pure_calls_impure_callable.py | 2 ++ tests/error/effects_errors/return_explicit_callable.py | 2 ++ tests/error/effects_errors/return_impure_callable.py | 2 ++ tests/error/effects_errors/return_pure_callable.py | 2 ++ 5 files changed, 10 insertions(+) diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.py b/tests/error/effects_errors/pure_calls_explicit_callable.py index 1f23bf5be..4db19383c 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.py +++ b/tests/error/effects_errors/pure_calls_explicit_callable.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from guppylang.decorator import guppy @guppy(effects=[]) diff --git a/tests/error/effects_errors/pure_calls_impure_callable.py b/tests/error/effects_errors/pure_calls_impure_callable.py index 9629bcbf6..eb8e4cb97 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.py +++ b/tests/error/effects_errors/pure_calls_impure_callable.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from guppylang.decorator import guppy @guppy(effects=[]) diff --git a/tests/error/effects_errors/return_explicit_callable.py b/tests/error/effects_errors/return_explicit_callable.py index 9319d045b..58cdf70f1 100644 --- a/tests/error/effects_errors/return_explicit_callable.py +++ b/tests/error/effects_errors/return_explicit_callable.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from guppylang.decorator import guppy, Effect @guppy(effects=[Effect.ANY]) diff --git a/tests/error/effects_errors/return_impure_callable.py b/tests/error/effects_errors/return_impure_callable.py index 6ed02bd56..14a031401 100644 --- a/tests/error/effects_errors/return_impure_callable.py +++ b/tests/error/effects_errors/return_impure_callable.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from guppylang.decorator import guppy @guppy diff --git a/tests/error/effects_errors/return_pure_callable.py b/tests/error/effects_errors/return_pure_callable.py index dcab1cf69..4f2552ad3 100644 --- a/tests/error/effects_errors/return_pure_callable.py +++ b/tests/error/effects_errors/return_pure_callable.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from guppylang.decorator import guppy @guppy(effects=[]) From 8cbd11a1474df4a870b5670a609d2e471f6a0af0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 16:39:01 +0100 Subject: [PATCH 52/81] ...and update error messages --- .../pure_calls_explicit_callable.err | 12 ++++++------ .../effects_errors/pure_calls_impure_callable.err | 12 ++++++------ .../effects_errors/return_explicit_callable.err | 14 +++++++------- .../effects_errors/return_impure_callable.err | 14 +++++++------- .../error/effects_errors/return_pure_callable.err | 8 ++++---- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 808b38e56..1b43a269d 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -1,15 +1,15 @@ -Error: Too many effects (at $FILE:5:10) +Error: Too many effects (at $FILE:7:10) | -3 | @guppy(effects=[]) -4 | def main(impure_f: Callable[[int], int, [ANY]]) -> int: -5 | return impure_f(5) +5 | @guppy(effects=[]) +6 | def main(impure_f: Callable[[int], int, [ANY]]) -> int: +7 | return impure_f(5) | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that | exceed those allowed Note: | -2 | -3 | @guppy(effects=[]) +4 | +5 | @guppy(effects=[]) | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 9f7aecd9f..277c2c2fc 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -1,15 +1,15 @@ -Error: Too many effects (at $FILE:5:10) +Error: Too many effects (at $FILE:7:10) | -3 | @guppy(effects=[]) -4 | def main(impure_f: Callable[[int], int]) -> int: -5 | return impure_f(5) +5 | @guppy(effects=[]) +6 | def main(impure_f: Callable[[int], int]) -> int: +7 | return impure_f(5) | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed | those allowed Note: | -2 | -3 | @guppy(effects=[]) +4 | +5 | @guppy(effects=[]) | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_explicit_callable.err b/tests/error/effects_errors/return_explicit_callable.err index 7d32fe210..a0073aecb 100644 --- a/tests/error/effects_errors/return_explicit_callable.err +++ b/tests/error/effects_errors/return_explicit_callable.err @@ -1,9 +1,9 @@ -Error: Type mismatch (at $FILE:9:10) - | -7 | @guppy -8 | def main() -> Callable[[int], int, []]: -9 | return impure_func - | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int - | -[ANY]-> int` +Error: Type mismatch (at $FILE:11:10) + | + 9 | @guppy +10 | def main() -> Callable[[int], int, []]: +11 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int + | -[ANY]-> int` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_impure_callable.err b/tests/error/effects_errors/return_impure_callable.err index 21e2e90e4..60b132daf 100644 --- a/tests/error/effects_errors/return_impure_callable.err +++ b/tests/error/effects_errors/return_impure_callable.err @@ -1,9 +1,9 @@ -Error: Type mismatch (at $FILE:9:10) - | -7 | @guppy -8 | def main() -> Callable[[int], int, []]: -9 | return impure_func - | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> - | int` +Error: Type mismatch (at $FILE:11:10) + | + 9 | @guppy +10 | def main() -> Callable[[int], int, []]: +11 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> + | int` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_pure_callable.err b/tests/error/effects_errors/return_pure_callable.err index c6cfb353c..e81b94dbe 100644 --- a/tests/error/effects_errors/return_pure_callable.err +++ b/tests/error/effects_errors/return_pure_callable.err @@ -1,8 +1,8 @@ -Error: Type mismatch (at $FILE:10:10) +Error: Type mismatch (at $FILE:12:10) | - 8 | @guppy - 9 | def main() -> Callable[[int], int]: -10 | return pure_func +10 | @guppy +11 | def main() -> Callable[[int], int]: +12 | return pure_func | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -[]-> | int` From 6163444993845c90916bc73268609b083728f159 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 17:05:30 +0100 Subject: [PATCH 53/81] make enum-constructors also effect-free --- guppylang-internals/src/guppylang_internals/definition/enum.py | 1 + tests/integration/notebooks/misc_notebook_tests.ipynb | 2 +- tests/integration/test_enum.py | 2 +- tests/integration/tracing/test_enum.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/enum.py b/guppylang-internals/src/guppylang_internals/definition/enum.py index 1bc17cf8b..bcfc20561 100644 --- a/guppylang-internals/src/guppylang_internals/definition/enum.py +++ b/guppylang-internals/src/guppylang_internals/definition/enum.py @@ -306,6 +306,7 @@ def compile(self, wires: list[Wire]) -> list[Wire]: ], output=enum_type, params=self.params, + declared_effects=[], ) constructor_def = CustomFunctionDef( diff --git a/tests/integration/notebooks/misc_notebook_tests.ipynb b/tests/integration/notebooks/misc_notebook_tests.ipynb index 097f4d4d8..518c17ead 100644 --- a/tests/integration/notebooks/misc_notebook_tests.ipynb +++ b/tests/integration/notebooks/misc_notebook_tests.ipynb @@ -243,7 +243,7 @@ "16 | return MyEnum.Var()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -> MyEnum`\n", + "Note: Function signature is `int -[]-> MyEnum`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index b302ba621..c3708aee5 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -247,7 +247,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: Callable[[int], Enum[int]], x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int], []], x: int) -> Enum[int]: return mk_enum(x) @guppy diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index dd3a16412..128f28b9a 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -107,7 +107,7 @@ class MyEnum: VariantA = {"x": int} @guppy.comptime - def test() -> Callable[[int], MyEnum]: + def test() -> Callable[[int], MyEnum, []]: return MyEnum.VariantA validate(test.compile_function()) From 328f8ded6f56d1a3bb32f69c4c09e3fc15f07aae Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 18:44:35 +0100 Subject: [PATCH 54/81] WIP @ effects. Referring to Effects.ANY is a PITA --- .../src/guppylang_internals/tys/parsing.py | 40 +++++++++---------- guppylang/src/guppylang/decorator.py | 2 +- guppylang/src/guppylang/std/builtins.py | 3 +- guppylang/src/guppylang/std/lang.py | 21 +++++++++- .../pure_calls_explicit_callable.py | 3 +- .../return_explicit_callable.py | 2 +- .../effects_errors/return_impure_callable.err | 8 ++-- .../effects_errors/return_impure_callable.py | 3 +- tests/error/modifier_errors/higher_order.py | 4 +- tests/error/poly_errors/non_linear2.py | 3 +- tests/integration/test_effects.py | 10 ++--- tests/integration/test_struct.py | 7 +++- tests/integration/tracing/test_enum.py | 3 +- tests/integration/tracing/test_struct.py | 3 +- 14 files changed, 68 insertions(+), 44 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index 64b095fac..bbc48c353 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -277,34 +277,16 @@ def _parse_delayed_annotation(ast_str: str, node: ast.Constant) -> ast.expr: def _parse_callable_type( args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx ) -> FunctionType: - """Helper function to parse a `Callable` type: - either `Callable[[], ]` - or `Callable[[], , ]`.""" + """Helper function to parse a `Callable[[], ]` type.""" err = InvalidCallableTypeError(loc) - if len(args) not in [2, 3]: + if len(args) != 2: raise GuppyError(err) - inputs = args[0] - output = args[1] + [inputs, output] = args if not isinstance(inputs, ast.List): raise GuppyError(err) inputs = [parse_function_arg_annotation(inp, None, ctx) for inp in inputs.elts] output = type_from_ast(output, ctx) - - effects: list[Effect] | None - if len(args) == 2: - effects = None - elif not isinstance(args[2], ast.List): - raise GuppyError(err) - else: - effects = [] - for e in args[2].elts: - if not isinstance(e, ast.Name): - raise GuppyError(err) - try: - effects.append(Effect.__from_str__(e.id)) - except ValueError: - raise GuppyError(err) # noqa: B904 - return FunctionType(inputs, output, declared_effects=effects) + return FunctionType(inputs, output) def _parse_self_type(args: list[ast.expr], loc: AstNode, ctx: TypeParsingCtx) -> Type: @@ -489,6 +471,20 @@ def type_with_flags_from_ast( flags |= InputFlags.Comptime if not ty.copyable or not ty.droppable: 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)) + if ty.declared_effects is not None: + raise GuppyError(InvalidFlagError(node.right)) + effects: list[Effect] = [] + for e in fx.args: + if not isinstance(e, ast.Name): + raise GuppyError(InvalidFlagError(node.right)) + try: + effects.append(Effect.__from_str__(e.id)) + except ValueError: + raise GuppyError(InvalidFlagError(node.right)) # noqa: B904 + ty = ty.with_effects(effects) case _: raise GuppyError(InvalidFlagError(node.right)) return ty, flags diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index c69772e2a..3cd4a27f3 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -95,7 +95,7 @@ OverloadedFunctionDef, ) -__all__ = ("GuppyKwargs", "custom_guppy_decorator", "guppy") +__all__ = ("Effect", "GuppyKwargs", "custom_guppy_decorator", "guppy") class Effect(Enum): diff --git a/guppylang/src/guppylang/std/builtins.py b/guppylang/src/guppylang/std/builtins.py index 8495be6a4..31cdbee44 100644 --- a/guppylang/src/guppylang/std/builtins.py +++ b/guppylang/src/guppylang/std/builtins.py @@ -3,7 +3,7 @@ from guppylang.std.array import ArrayIter, FrozenarrayIter, array, frozenarray from guppylang.std.bool import bool from guppylang.std.iter import Range, SizedIter, range -from guppylang.std.lang import comptime, control, dagger, owned, power, py +from guppylang.std.lang import comptime, control, dagger, effects, owned, power, py from guppylang.std.list import list from guppylang.std.mem import mem_swap from guppylang.std.num import ( @@ -108,6 +108,7 @@ "dict", "dir", "divmod", + "effects", "enumerate", "eval", "exec", diff --git a/guppylang/src/guppylang/std/lang.py b/guppylang/src/guppylang/std/lang.py index 3878eec86..7e9335144 100644 --- a/guppylang/src/guppylang/std/lang.py +++ b/guppylang/src/guppylang/std/lang.py @@ -1,10 +1,13 @@ """Provides Python objects for builtin language keywords.""" from collections.abc import Generator -from typing import Any, Protocol, TypeVar +from typing import TYPE_CHECKING, Any, Protocol, TypeVar from guppylang_internals.error import GuppyComptimeError +if TYPE_CHECKING: + from guppylang.decorator import Effect + T = TypeVar("T") _MODIFIER_COMPTIME_ERROR = ( @@ -43,6 +46,22 @@ def __rmatmul__(self, other: Any) -> Any: owned = _Owned() +class Effects: + """Dummy class to support `@effects` annotations.""" + + effects: list["Effect"] + + def __init__(self, *effects: "Effect") -> None: + self.effects = list(effects) + + def __rmatmul__(self, other: Any) -> Any: + # This method is to make the Python interpreter happy with @comptime at runtime + return other + + +effects = Effects + + class Copy(Protocol): """Bound to mark generic type parameters as being implicitly copyable.""" diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.py b/tests/error/effects_errors/pure_calls_explicit_callable.py index 4db19383c..d8096bd05 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.py +++ b/tests/error/effects_errors/pure_calls_explicit_callable.py @@ -1,9 +1,10 @@ from collections.abc import Callable from guppylang.decorator import guppy +from guppylang.std.builtins import effects @guppy(effects=[]) -def main(impure_f: Callable[[int], int, [ANY]]) -> int: +def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: return impure_f(5) main.compile() diff --git a/tests/error/effects_errors/return_explicit_callable.py b/tests/error/effects_errors/return_explicit_callable.py index 58cdf70f1..046f1e9e1 100644 --- a/tests/error/effects_errors/return_explicit_callable.py +++ b/tests/error/effects_errors/return_explicit_callable.py @@ -7,7 +7,7 @@ def impure_func(x: int) -> int: return x + 1 @guppy -def main() -> Callable[[int], int, []]: +def main() -> Callable[[int], int] @effects(): return impure_func main.compile() diff --git a/tests/error/effects_errors/return_impure_callable.err b/tests/error/effects_errors/return_impure_callable.err index 60b132daf..05d5b87ad 100644 --- a/tests/error/effects_errors/return_impure_callable.err +++ b/tests/error/effects_errors/return_impure_callable.err @@ -1,8 +1,8 @@ -Error: Type mismatch (at $FILE:11:10) +Error: Type mismatch (at $FILE:12:10) | - 9 | @guppy -10 | def main() -> Callable[[int], int, []]: -11 | return impure_func +10 | @guppy +11 | def main() -> Callable[[int], int] @ effects(): +12 | return impure_func | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> | int` diff --git a/tests/error/effects_errors/return_impure_callable.py b/tests/error/effects_errors/return_impure_callable.py index 14a031401..574625dd3 100644 --- a/tests/error/effects_errors/return_impure_callable.py +++ b/tests/error/effects_errors/return_impure_callable.py @@ -1,13 +1,14 @@ from collections.abc import Callable from guppylang.decorator import guppy +from guppylang.std.builtins import effects @guppy def impure_func(x: int) -> int: return x + 1 @guppy -def main() -> Callable[[int], int, []]: +def main() -> Callable[[int], int] @ effects(): return impure_func main.compile() \ No newline at end of file diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index 4b1ace90b..bb9ace105 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -1,12 +1,12 @@ from guppylang.decorator import guppy -from guppylang.std.builtins import dagger +from guppylang.std.builtins import dagger, effects from guppylang.std.quantum import qubit, h, discard from collections.abc import Callable # f would need a flag to be used in dagger context, but no way to specify that yet @guppy(dagger=True) -def test_ho(f: Callable[[qubit], None, []], q: qubit) -> None: +def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: f(q) diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index e640aca95..6c4b0bd1d 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -2,6 +2,7 @@ from guppylang.decorator import guppy from guppylang.std.quantum.functional import h +from guppylang.std.builtins import effects T = guppy.type_var("T") @@ -9,7 +10,7 @@ # Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly # declare the effects of `f` @guppy.declare -def foo(x: Callable[[T], T, []]) -> None: ... +def foo(x: Callable[[T], T] @ effects()) -> None: ... @guppy diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index ea33fc571..853ccd3a2 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -4,7 +4,7 @@ from collections.abc import Callable from guppylang.decorator import guppy, Effect -from guppylang.std.builtins import result +from guppylang.std.builtins import effects, result def test_pure_decl_from_impure(validate): @@ -118,7 +118,7 @@ def pure_func2(x: int) -> int: def test_pure_callable_from_impure(validate): @guppy - def impure_func(pure_f: Callable[[int], int, []]) -> int: + def impure_func(pure_f: Callable[[int], int] @ effects()) -> int: return pure_f(5) + 1 validate(impure_func.compile_function()) @@ -126,7 +126,7 @@ def impure_func(pure_f: Callable[[int], int, []]) -> int: def test_pure_callable_from_pure(validate): @guppy(effects=[]) - def pure_func(pure_f: Callable[[int], int, []]) -> int: + def pure_func(pure_f: Callable[[int], int] @ effects()) -> int: return pure_f(5) + 1 validate(pure_func.compile_function()) @@ -134,7 +134,7 @@ def pure_func(pure_f: Callable[[int], int, []]) -> int: def test_pure_callable_from_impure_explicit(validate): @guppy(effects=[Effect.ANY]) - def impure_func(pure_f: Callable[[int], int, []]) -> int: + def impure_func(pure_f: Callable[[int], int] @ effects()) -> int: return pure_f(5) + 1 validate(impure_func.compile_function()) @@ -146,7 +146,7 @@ def impure_func(x: int) -> int: return x + 1 @guppy(effects=[]) - def higher_order() -> Callable[[int], int, [ANY]]: # noqa: F821 + def higher_order() -> Callable[[int], int] @ effects(Effect.ANY): return impure_func validate(higher_order.compile_function()) diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index a6875c743..3386c165f 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -2,10 +2,11 @@ from guppylang.decorator import guppy - if TYPE_CHECKING: from collections.abc import Callable + from guppylang.std.builtins import effects + def test_basic_defs(validate): @guppy.struct @@ -136,7 +137,9 @@ class Struct(Generic[T]): # Pending https://github.com/Quantinuum/guppylang/issues/1760 # we must explicitly state the effects of `mk_struct` @guppy - def factory(mk_struct: "Callable[[int], Struct[int], []]", x: int) -> Struct[int]: + def factory( + mk_struct: "Callable[[int], Struct[int]] @ effects()", x: int + ) -> Struct[int]: return mk_struct(x) @guppy diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index 128f28b9a..4b66b032f 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -2,6 +2,7 @@ from typing import Generic from guppylang.decorator import guppy +from guppylang.std.builtins import effects def test_create(validate): @@ -107,7 +108,7 @@ class MyEnum: VariantA = {"x": int} @guppy.comptime - def test() -> Callable[[int], MyEnum, []]: + def test() -> Callable[[int], MyEnum] @ effects(): return MyEnum.VariantA validate(test.compile_function()) diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index c3d4cd561..baa4b8069 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -2,6 +2,7 @@ from typing import Generic from guppylang.decorator import guppy +from guppylang.std.builtins import effects def test_create(run_int_fn): @@ -114,7 +115,7 @@ class S: x: int @guppy.comptime - def test() -> Callable[[int], S, []]: + def test() -> Callable[[int], S] @ effects(): return S validate(test.compile_function()) From 7e80ed858a58c50fad14413887ca1a81aa671287 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 18:55:41 +0100 Subject: [PATCH 55/81] move @effects to std/effects.py --- guppylang/src/guppylang/std/builtins.py | 3 +-- guppylang/src/guppylang/std/effects.py | 21 +++++++++++++++++++ guppylang/src/guppylang/std/lang.py | 21 +------------------ .../pure_calls_explicit_callable.err | 12 +++++------ .../pure_calls_explicit_callable.py | 2 +- .../return_explicit_callable.err | 8 +++---- .../return_explicit_callable.py | 1 + .../effects_errors/return_impure_callable.py | 2 +- tests/error/poly_errors/non_linear2.py | 2 +- tests/integration/test_effects.py | 5 +++-- tests/integration/test_struct.py | 2 +- tests/integration/tracing/test_enum.py | 2 +- tests/integration/tracing/test_struct.py | 2 +- 13 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 guppylang/src/guppylang/std/effects.py diff --git a/guppylang/src/guppylang/std/builtins.py b/guppylang/src/guppylang/std/builtins.py index 31cdbee44..8495be6a4 100644 --- a/guppylang/src/guppylang/std/builtins.py +++ b/guppylang/src/guppylang/std/builtins.py @@ -3,7 +3,7 @@ from guppylang.std.array import ArrayIter, FrozenarrayIter, array, frozenarray from guppylang.std.bool import bool from guppylang.std.iter import Range, SizedIter, range -from guppylang.std.lang import comptime, control, dagger, effects, owned, power, py +from guppylang.std.lang import comptime, control, dagger, owned, power, py from guppylang.std.list import list from guppylang.std.mem import mem_swap from guppylang.std.num import ( @@ -108,7 +108,6 @@ "dict", "dir", "divmod", - "effects", "enumerate", "eval", "exec", diff --git a/guppylang/src/guppylang/std/effects.py b/guppylang/src/guppylang/std/effects.py new file mode 100644 index 000000000..d7531e911 --- /dev/null +++ b/guppylang/src/guppylang/std/effects.py @@ -0,0 +1,21 @@ +from typing import Any + +from guppylang.decorator import Effect + + +class Effects: + """Dummy class to support `@effects` annotations.""" + + effects: list[Effect] + + def __init__(self, *effects: Effect) -> None: + self.effects = list(effects) + + def __rmatmul__(self, other: Any) -> Any: + # This method is to make the Python interpreter happy with @comptime at runtime + return other + + +effects = Effects + +ANY = Effect.ANY diff --git a/guppylang/src/guppylang/std/lang.py b/guppylang/src/guppylang/std/lang.py index 7e9335144..3878eec86 100644 --- a/guppylang/src/guppylang/std/lang.py +++ b/guppylang/src/guppylang/std/lang.py @@ -1,13 +1,10 @@ """Provides Python objects for builtin language keywords.""" from collections.abc import Generator -from typing import TYPE_CHECKING, Any, Protocol, TypeVar +from typing import Any, Protocol, TypeVar from guppylang_internals.error import GuppyComptimeError -if TYPE_CHECKING: - from guppylang.decorator import Effect - T = TypeVar("T") _MODIFIER_COMPTIME_ERROR = ( @@ -46,22 +43,6 @@ def __rmatmul__(self, other: Any) -> Any: owned = _Owned() -class Effects: - """Dummy class to support `@effects` annotations.""" - - effects: list["Effect"] - - def __init__(self, *effects: "Effect") -> None: - self.effects = list(effects) - - def __rmatmul__(self, other: Any) -> Any: - # This method is to make the Python interpreter happy with @comptime at runtime - return other - - -effects = Effects - - class Copy(Protocol): """Bound to mark generic type parameters as being implicitly copyable.""" diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 1b43a269d..157c8a2df 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -1,15 +1,15 @@ -Error: Too many effects (at $FILE:7:10) +Error: Too many effects (at $FILE:8:10) | -5 | @guppy(effects=[]) -6 | def main(impure_f: Callable[[int], int, [ANY]]) -> int: -7 | return impure_f(5) +6 | @guppy(effects=[]) +7 | def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: +8 | return impure_f(5) | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that | exceed those allowed Note: | -4 | -5 | @guppy(effects=[]) +5 | +6 | @guppy(effects=[]) | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.py b/tests/error/effects_errors/pure_calls_explicit_callable.py index d8096bd05..d3a5a60d1 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.py +++ b/tests/error/effects_errors/pure_calls_explicit_callable.py @@ -1,7 +1,7 @@ from collections.abc import Callable from guppylang.decorator import guppy -from guppylang.std.builtins import effects +from guppylang.std.effects import effects, ANY @guppy(effects=[]) def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: diff --git a/tests/error/effects_errors/return_explicit_callable.err b/tests/error/effects_errors/return_explicit_callable.err index a0073aecb..4d5d950d1 100644 --- a/tests/error/effects_errors/return_explicit_callable.err +++ b/tests/error/effects_errors/return_explicit_callable.err @@ -1,8 +1,8 @@ -Error: Type mismatch (at $FILE:11:10) +Error: Type mismatch (at $FILE:12:10) | - 9 | @guppy -10 | def main() -> Callable[[int], int, []]: -11 | return impure_func +10 | @guppy +11 | def main() -> Callable[[int], int] @effects(): +12 | return impure_func | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int | -[ANY]-> int` diff --git a/tests/error/effects_errors/return_explicit_callable.py b/tests/error/effects_errors/return_explicit_callable.py index 046f1e9e1..f3a215aa5 100644 --- a/tests/error/effects_errors/return_explicit_callable.py +++ b/tests/error/effects_errors/return_explicit_callable.py @@ -1,6 +1,7 @@ from collections.abc import Callable from guppylang.decorator import guppy, Effect +from guppylang.std.effects import effects @guppy(effects=[Effect.ANY]) def impure_func(x: int) -> int: diff --git a/tests/error/effects_errors/return_impure_callable.py b/tests/error/effects_errors/return_impure_callable.py index 574625dd3..928e45061 100644 --- a/tests/error/effects_errors/return_impure_callable.py +++ b/tests/error/effects_errors/return_impure_callable.py @@ -1,7 +1,7 @@ from collections.abc import Callable from guppylang.decorator import guppy -from guppylang.std.builtins import effects +from guppylang.std.effects import effects @guppy def impure_func(x: int) -> int: diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index 6c4b0bd1d..598f67a4c 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -2,7 +2,7 @@ from guppylang.decorator import guppy from guppylang.std.quantum.functional import h -from guppylang.std.builtins import effects +from guppylang.std.effects import effects T = guppy.type_var("T") diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index 853ccd3a2..753c32cff 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -4,7 +4,8 @@ from collections.abc import Callable from guppylang.decorator import guppy, Effect -from guppylang.std.builtins import effects, result +from guppylang.std.builtins import result +from guppylang.std.effects import effects, ANY def test_pure_decl_from_impure(validate): @@ -146,7 +147,7 @@ def impure_func(x: int) -> int: return x + 1 @guppy(effects=[]) - def higher_order() -> Callable[[int], int] @ effects(Effect.ANY): + def higher_order() -> Callable[[int], int] @ effects(ANY): return impure_func validate(higher_order.compile_function()) diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index 3386c165f..42c85699d 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from guppylang.std.builtins import effects + from guppylang.std.effects import effects def test_basic_defs(validate): diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index 4b66b032f..ecc5e8ec0 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -2,7 +2,7 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.builtins import effects +from guppylang.std.effects import effects def test_create(validate): diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index baa4b8069..77e1d647f 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -2,7 +2,7 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.builtins import effects +from guppylang.std.effects import effects def test_create(run_int_fn): From 6976d3b73b5886f847013babe12d13f791835103 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 19:24:51 +0100 Subject: [PATCH 56/81] comments --- .../src/guppylang_internals/checker/expr_checker.py | 6 ++---- guppylang-internals/src/guppylang_internals/tys/parsing.py | 2 ++ guppylang-internals/src/guppylang_internals/tys/ty.py | 2 +- guppylang/src/guppylang/std/effects.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index e9f1945cc..33c693e10 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1355,9 +1355,8 @@ 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 last so we can avoid using them to resolve overloading _check_effects(func_ty, ctx, node) return args, unquantified.output.substitute(subst), inst @@ -1444,9 +1443,8 @@ 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 last so we can avoid using them to resolve overloading _check_effects(func_ty, ctx, node) return inputs, subst, inst diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index bbc48c353..f07af59e5 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -478,6 +478,8 @@ def type_with_flags_from_ast( raise GuppyError(InvalidFlagError(node.right)) effects: list[Effect] = [] for e in fx.args: + # We might want to support ast.Attribute with LHS "Effects" + # and look at RHS if not isinstance(e, ast.Name): raise GuppyError(InvalidFlagError(node.right)) try: diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index f5811248a..43a68d333 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -421,7 +421,7 @@ class FunctionType(ParametrizedTypeBase): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, init=True) - """ Effects declared in source code (i.e. as third argument to Callable). + """ Effects declared in source code, i.e. `Callable[...] @ effects(EFFECTS)`. None means there was no declaration, which is equivalent to [Effect.ANY] except for error reporting. Generally use `effects` instead.""" declared_effects: list[Effect] | None = field(default=None, init=True) diff --git a/guppylang/src/guppylang/std/effects.py b/guppylang/src/guppylang/std/effects.py index d7531e911..98808bc1b 100644 --- a/guppylang/src/guppylang/std/effects.py +++ b/guppylang/src/guppylang/std/effects.py @@ -12,7 +12,7 @@ def __init__(self, *effects: Effect) -> None: self.effects = list(effects) def __rmatmul__(self, other: Any) -> Any: - # This method is to make the Python interpreter happy with @comptime at runtime + # This method is to make the Python interpreter happy return other From 4689bd4656e5573b94a5ac1251afb6ff54ab65ef Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 19:37:22 +0100 Subject: [PATCH 57/81] missed from move of @effects --- tests/error/modifier_errors/higher_order.err | 8 ++++---- tests/error/modifier_errors/higher_order.py | 3 ++- tests/error/poly_errors/non_linear2.err | 8 ++++---- tests/integration/test_enum.py | 3 ++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/error/modifier_errors/higher_order.err b/tests/error/modifier_errors/higher_order.err index ad2fe9de6..d9e326b4d 100644 --- a/tests/error/modifier_errors/higher_order.err +++ b/tests/error/modifier_errors/higher_order.err @@ -1,8 +1,8 @@ -Error: Dagger constraint violation (at $FILE:10:4) +Error: Dagger constraint violation (at $FILE:11:4) | - 8 | @guppy(dagger=True) - 9 | def test_ho(f: Callable[[qubit], None, []], q: qubit) -> None: -10 | f(q) + 9 | @guppy(dagger=True) +10 | def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: +11 | f(q) | ^^^^ This function cannot be called in a dagger context Guppy compilation failed due to 1 previous error diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index bb9ace105..a36e8244a 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -1,5 +1,6 @@ from guppylang.decorator import guppy -from guppylang.std.builtins import dagger, effects +from guppylang.std.builtins import dagger +from guppylang.std.effects import effects from guppylang.std.quantum import qubit, h, discard from collections.abc import Callable diff --git a/tests/error/poly_errors/non_linear2.err b/tests/error/poly_errors/non_linear2.err index c27751838..9b3a8bc58 100644 --- a/tests/error/poly_errors/non_linear2.err +++ b/tests/error/poly_errors/non_linear2.err @@ -1,8 +1,8 @@ -Error: Expected a copyable type (at $FILE:17:8) +Error: Expected a copyable type (at $FILE:18:8) | -15 | @guppy -16 | def main() -> None: -17 | foo(h) +16 | @guppy +17 | def main() -> None: +18 | foo(h) | ^ Expected a copyable type, got type `qubit` which is not | implicitly copyable diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index c3708aee5..b5ff65ed5 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -21,6 +21,7 @@ """ from guppylang import guppy +from guppylang.std.effects import effects from tests.util import compile_guppy from typing import Generic @@ -247,7 +248,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: Callable[[int], Enum[int], []], x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int]] @ effects(), x: int) -> Enum[int]: return mk_enum(x) @guppy From af1f2e5ddedcd614f18591d93054b568389e545e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 19:47:30 +0100 Subject: [PATCH 58/81] missing import in fun_ty_mismatch_4 --- tests/error/type_errors/fun_ty_mismatch_4.err | 8 ++++---- tests/error/type_errors/fun_ty_mismatch_4.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/error/type_errors/fun_ty_mismatch_4.err b/tests/error/type_errors/fun_ty_mismatch_4.err index 72b625967..eff122445 100644 --- a/tests/error/type_errors/fun_ty_mismatch_4.err +++ b/tests/error/type_errors/fun_ty_mismatch_4.err @@ -1,8 +1,8 @@ -Error: Type mismatch (at $FILE:12:11) +Error: Type mismatch (at $FILE:14:11) | -10 | return x -11 | -12 | return bar +12 | return x +13 | +14 | return bar | ^^^ Expected return value of type `nat -> int`, got `nat -> nat` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/fun_ty_mismatch_4.py b/tests/error/type_errors/fun_ty_mismatch_4.py index 386b2df4b..fbd36be49 100644 --- a/tests/error/type_errors/fun_ty_mismatch_4.py +++ b/tests/error/type_errors/fun_ty_mismatch_4.py @@ -1,5 +1,7 @@ from collections.abc import Callable +from guppylang.std.builtins import nat + from tests.util import compile_guppy From 51745bfdb8c55a24c57d783a0a5d1b91c3613781 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 20:54:59 +0100 Subject: [PATCH 59/81] undo stdlib changes, enum+struct constructors, bool conversion --- .../checker/expr_checker.py | 7 +- .../guppylang_internals/definition/enum.py | 1 - .../guppylang_internals/definition/struct.py | 1 - guppylang/src/guppylang/std/angles.py | 18 +- guppylang/src/guppylang/std/num.py | 245 ++++++++---------- .../src/guppylang/std/qsystem/__init__.py | 5 +- .../src/guppylang/std/quantum/__init__.py | 50 ++-- .../src/guppylang/std/quantum/functional.py | 42 +-- tests/error/modifier_errors/higher_order.err | 8 +- tests/error/modifier_errors/higher_order.py | 3 +- tests/error/poly_errors/non_linear2.py | 2 +- .../struct_errors/constructor_missing_arg.err | 2 +- .../constructor_too_many_args.err | 2 +- tests/error/type_errors/and_not_bool_left.err | 2 +- .../error/type_errors/and_not_bool_right.err | 2 +- tests/error/type_errors/if_expr_not_bool.err | 2 +- tests/error/type_errors/if_not_bool.err | 2 +- tests/error/type_errors/not_not_bool.err | 2 +- tests/error/type_errors/or_not_bool_left.err | 2 +- tests/error/type_errors/or_not_bool_right.err | 2 +- tests/error/type_errors/while_not_bool.err | 2 +- .../notebooks/misc_notebook_tests.ipynb | 4 +- tests/integration/test_enum.py | 3 +- tests/integration/test_struct.py | 8 +- tests/integration/tracing/test_enum.py | 3 +- tests/integration/tracing/test_struct.py | 3 +- 26 files changed, 190 insertions(+), 233 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 33c693e10..1fc07e655 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1517,9 +1517,6 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type return node, node_ty synth = ExprSynthesizer(ctx) exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Inout)], bool_type()) - # 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([]) try: return synth.synthesize_instance_func( node, [], "__bool__", "truthy", exp_sig, True @@ -1528,9 +1525,7 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type if not node_ty.copyable: # Linear types may implement a `__consume_as_bool__` method that consumes # the value, instead of borrowing it. - exp_sig = FunctionType( - [FuncInput(node_ty, InputFlags.Owned)], bool_type() - ).with_effects([]) + exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Owned)], bool_type()) return synth.synthesize_instance_func( node, [], "__consume_as_bool__", "truthy", exp_sig, True ) diff --git a/guppylang-internals/src/guppylang_internals/definition/enum.py b/guppylang-internals/src/guppylang_internals/definition/enum.py index bcfc20561..1bc17cf8b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/enum.py +++ b/guppylang-internals/src/guppylang_internals/definition/enum.py @@ -306,7 +306,6 @@ def compile(self, wires: list[Wire]) -> list[Wire]: ], output=enum_type, params=self.params, - declared_effects=[], ) constructor_def = CustomFunctionDef( diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index ff40842c9..ae753cb3b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -225,7 +225,6 @@ def compile(self, args: list[Wire]) -> list[Wire]: defn=self, args=[p.to_bound(i) for i, p in enumerate(self.params)] ), params=self.params, - declared_effects=[], ) constructor_def = CustomFunctionDef( id=DefId.fresh(), diff --git a/guppylang/src/guppylang/std/angles.py b/guppylang/src/guppylang/std/angles.py index 4b4fb8f5c..d44aea0b0 100644 --- a/guppylang/src/guppylang/std/angles.py +++ b/guppylang/src/guppylang/std/angles.py @@ -31,47 +31,47 @@ class angle: halfturns: float - @guppy(effects=[]) + @guppy @no_type_check def __add__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns + other.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __sub__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns - other.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __mul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(effects=[]) + @guppy @no_type_check def __rmul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(effects=[]) + @guppy @no_type_check def __truediv__(self: "angle", other: float) -> "angle": return angle(self.halfturns / other) - @guppy(effects=[]) + @guppy @no_type_check def __rtruediv__(self: "angle", other: float) -> "angle": return angle(other / self.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __neg__(self: "angle") -> "angle": return angle(-self.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __float__(self: "angle") -> float: return self.halfturns * py(math.pi) - @guppy(effects=[]) + @guppy @no_type_check def __eq__(self: "angle", other: "angle") -> bool: return self.halfturns == other.halfturns diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 7b7ebc263..631abd7c7 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -20,99 +20,94 @@ ) from guppylang_internals.tys.builtin import float_type_def, int_type_def, nat_type_def -from guppylang import Effect, guppy +from guppylang import guppy @extend_type(nat_type_def) class nat: """A 64-bit unsigned integer.""" - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __abs__(self: nat) -> nat: ... - @hugr_op(int_op("iadd"), effects=[]) + @hugr_op(int_op("iadd")) def __add__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("iand"), effects=[]) + @hugr_op(int_op("iand")) def __and__(self: nat, other: nat) -> nat: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: nat) -> bool: return self != 0 - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __ceil__(self: nat) -> nat: ... - # Panics if other == 0 - @hugr_op(int_op("idivmod_u", n_vars=2), effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_u", n_vars=2)) def __divmod__(self: nat, other: nat) -> tuple[nat, nat]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq"))) def __eq__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) + @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION)) def __float__(self: nat) -> float: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __floor__(self: nat) -> nat: ... - # Panics if other == 0 - @hugr_op(int_op("idiv_u"), effects=[Effect.ANY]) + @hugr_op(int_op("idiv_u")) def __floordiv__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ige_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_u"))) def __ge__(self: nat, other: nat) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_u"))) def __gt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("iu_to_s"), effects=[]) + @hugr_op(int_op("iu_to_s")) def __int__(self: nat) -> int: ... - @hugr_op(int_op("inot"), effects=[]) + @hugr_op(int_op("inot")) def __invert__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ile_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_u"))) def __le__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("ishl"), effects=[]) + @hugr_op(int_op("ishl")) def __lshift__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ilt_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ilt_u"))) def __lt__(self: nat, other: nat) -> bool: ... - # Panics if other == 0 - @hugr_op(int_op("imod_u"), effects=[Effect.ANY]) + @hugr_op(int_op("imod_u")) def __mod__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("imul"), effects=[]) + @hugr_op(int_op("imul")) def __mul__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __nat__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ine"))) def __ne__(self: nat, other: nat) -> bool: ... - @custom_function( - checker=DunderChecker("__nat__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__nat__"), higher_order_value=False) def __new__(x): ... - @hugr_op(int_op("ior"), effects=[]) + @hugr_op(int_op("ior")) def __or__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: nat) -> nat: ... - @hugr_op(int_op("ipow"), effects=[]) + @hugr_op(int_op("ipow")) def __pow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rand__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) @@ -121,40 +116,40 @@ def __rdivmod__(self: nat, other: nat) -> tuple[nat, nat]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rlshift__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __ror__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __round__(self: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rpow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rrshift__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("ishr"), effects=[]) + @hugr_op(int_op("ishr")) def __rshift__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rtruediv__(self: nat, other: nat) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rxor__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("isub"), effects=[]) + @hugr_op(int_op("isub")) def __sub__(self: nat, other: nat) -> nat: ... @guppy @@ -162,10 +157,10 @@ def __sub__(self: nat, other: nat) -> nat: ... def __truediv__(self: nat, other: nat) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __trunc__(self: nat) -> nat: ... - @hugr_op(int_op("ixor"), effects=[]) + @hugr_op(int_op("ixor")) def __xor__(self: nat, other: nat) -> nat: ... @@ -173,56 +168,51 @@ def __xor__(self: nat, other: nat) -> nat: ... class int: """A 64-bit signed integer.""" - @hugr_op( - int_op("iabs"), # TODO: Maybe wrong? (signed vs unsigned!) - effects=[], - ) + @hugr_op(int_op("iabs")) # TODO: Maybe wrong? (signed vs unsigned!) def __abs__(self: int) -> int: ... - @hugr_op(int_op("iadd"), effects=[]) + @hugr_op(int_op("iadd")) def __add__(self: int, other: int) -> int: ... - @hugr_op(int_op("iand"), effects=[]) + @hugr_op(int_op("iand")) def __and__(self: int, other: int) -> int: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: int) -> bool: return self != 0 - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __ceil__(self: int) -> int: ... - # Panics if other == 0 - @hugr_op(int_op("idivmod_s"), effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_s")) def __divmod__(self: int, other: int) -> tuple[int, int]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq"))) def __eq__(self: int, other: int) -> bool: ... - @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) + @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION)) def __float__(self: int) -> float: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __floor__(self: int) -> int: ... - # Panics if other == 0 - @hugr_op(int_op("idiv_s"), effects=[Effect.ANY]) + @hugr_op(int_op("idiv_s")) def __floordiv__(self: int, other: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ige_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_s"))) def __ge__(self: int, other: int) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_s"))) def __gt__(self: int, other: int) -> bool: ... @custom_function(NoopCompiler()) def __int__(self: int) -> int: ... - @hugr_op(int_op("inot"), effects=[]) + @hugr_op(int_op("inot")) def __invert__(self: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ile_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_s"))) def __le__(self: int, other: int) -> bool: ... @hugr_op(int_op("ishl")) # TODO: RHS is unsigned @@ -231,30 +221,28 @@ def __lshift__(self: int, other: int) -> int: ... @custom_function(BoolOpCompiler(int_op("ilt_s"))) def __lt__(self: int, other: int) -> bool: ... - @hugr_op(int_op("imod_s"), effects=[]) + @hugr_op(int_op("imod_s")) def __mod__(self: int, other: int) -> int: ... - @hugr_op(int_op("imul"), effects=[]) + @hugr_op(int_op("imul")) def __mul__(self: int, other: int) -> int: ... - @hugr_op(int_op("is_to_u"), effects=[]) + @hugr_op(int_op("is_to_u")) def __nat__(self: int) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ine"))) def __ne__(self: int, other: int) -> bool: ... - @hugr_op(int_op("ineg"), effects=[]) + @hugr_op(int_op("ineg")) def __neg__(self: int) -> int: ... - @custom_function( - checker=DunderChecker("__int__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__int__"), higher_order_value=False) def __new__(x): ... - @hugr_op(int_op("ior"), effects=[]) + @hugr_op(int_op("ior")) def __or__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: int) -> int: ... @guppy @@ -267,13 +255,13 @@ def __pow__(self: int, exponent: int) -> int: ) return self.__pow_impl(exponent) - @hugr_op(int_op("ipow"), effects=[]) # Exponent is treated as unsigned + @hugr_op(int_op("ipow")) def __pow_impl(self: int, exponent: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rand__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -282,23 +270,19 @@ def __rdivmod__(self: int, other: int) -> tuple[int, int]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: int, other: int) -> int: ... - @custom_function( - checker=ReversingChecker(), # TODO: RHS is unsigned - effects=[], - ) + @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rlshift__(self: int, other: int) -> int: ... - # What if other==0 ? @custom_function(checker=ReversingChecker()) def __rmod__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __ror__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __round__(self: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -307,33 +291,30 @@ def __rpow__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rrshift__(self: int, other: int) -> int: ... - @hugr_op( - int_op("ishr"), # TODO: RHS is unsigned - effects=[], - ) + @hugr_op(int_op("ishr")) # TODO: RHS is unsigned def __rshift__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rtruediv__(self: int, other: int) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rxor__(self: int, other: int) -> int: ... - @hugr_op(int_op("isub"), effects=[]) + @hugr_op(int_op("isub")) def __sub__(self: int, other: int) -> int: ... - @guppy(effects=[]) + @guppy @no_type_check def __truediv__(self: int, other: int) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __trunc__(self: int) -> int: ... - @hugr_op(int_op("ixor"), effects=[]) + @hugr_op(int_op("ixor")) def __xor__(self: int, other: int) -> int: ... @@ -341,128 +322,124 @@ def __xor__(self: int, other: int) -> int: ... class float: """An IEEE754 double-precision floating point value.""" - @hugr_op(float_op("fabs"), effects=[]) + @hugr_op(float_op("fabs")) def __abs__(self: float) -> float: ... - @hugr_op(float_op("fadd"), effects=[]) + @hugr_op(float_op("fadd")) def __add__(self: float, other: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: float) -> bool: return self != 0.0 - @hugr_op(float_op("fceil"), effects=[]) + @hugr_op(float_op("fceil")) def __ceil__(self: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __divmod__(self: float, other: float) -> tuple[float, float]: return self // other, self.__mod__(other) - @custom_function(BoolOpCompiler(float_op("feq")), effects=[]) + @custom_function(BoolOpCompiler(float_op("feq"))) def __eq__(self: float, other: float) -> bool: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __float__(self: float) -> float: ... - @hugr_op(float_op("ffloor"), effects=[]) + @hugr_op(float_op("ffloor")) def __floor__(self: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __floordiv__(self: float, other: float) -> float: return (self / other).__floor__() - @custom_function(BoolOpCompiler(float_op("fge")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fge"))) def __ge__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("fgt")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fgt"))) def __gt__(self: float, other: float) -> bool: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_s", hugr.std.int.CONVERSIONS_EXTENSION), - ), - effects=[], + ) ) def __int__(self: float) -> int: ... - @custom_function(BoolOpCompiler(float_op("fle")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fle"))) def __le__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("flt")), effects=[]) + @custom_function(BoolOpCompiler(float_op("flt"))) def __lt__(self: float, other: float) -> bool: ... - @guppy(effects=[]) + @guppy @no_type_check def __mod__(self: float, other: float) -> float: return self - (self // other) * other - @hugr_op(float_op("fmul"), effects=[]) + @hugr_op(float_op("fmul")) def __mul__(self: float, other: float) -> float: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_u", hugr.std.int.CONVERSIONS_EXTENSION), - ), - effects=[], + ) ) def __nat__(self: float) -> nat: ... - @custom_function(BoolOpCompiler(float_op("fne")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fne"))) def __ne__(self: float, other: float) -> bool: ... - @hugr_op(float_op("fneg"), effects=[]) + @hugr_op(float_op("fneg")) def __neg__(self: float) -> float: ... - @custom_function( - checker=DunderChecker("__float__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__float__"), higher_order_value=False) def __new__(x): ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: float) -> float: ... - @hugr_op(float_op("fpow"), effects=[]) # TODO + @hugr_op(float_op("fpow")) # TODO def __pow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rdivmod__(self: float, other: float) -> tuple[float, float]: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmod__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: float, other: float) -> float: ... - @hugr_op(float_op("fround"), effects=[]) # TODO + @hugr_op(float_op("fround")) # TODO def __round__(self: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rpow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rtruediv__(self: float, other: float) -> float: ... - @hugr_op(float_op("fsub"), effects=[]) + @hugr_op(float_op("fsub")) def __sub__(self: float, other: float) -> float: ... - @hugr_op(float_op("fdiv"), effects=[]) + @hugr_op(float_op("fdiv")) def __truediv__(self: float, other: float) -> float: ... - @hugr_op(unsupported_op("trunc_s"), effects=[]) # TODO `trunc_s` returns an option + @hugr_op(unsupported_op("trunc_s")) # TODO `trunc_s` returns an option def __trunc__(self: float) -> float: ... diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index e6c0d23f0..b2ab03655 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -256,15 +256,14 @@ class Measurement: """Represents the result of a lazy measurement which needs to be explicitly read before being used.""" - # We do *not* model the pipeline as a side-effect - @custom_function(compiler=ReadFutureBoolCompiler(), effects=[]) + @custom_function(compiler=ReadFutureBoolCompiler()) @no_type_check def read(self: "Measurement" @ owned) -> bool: """Read the measurement result, consuming it. Blocks until the result is available if the measurement hasn't been performed yet since being requested. """ - @guppy(effects=[]) + @guppy @no_type_check def __consume_as_bool__(self: "Measurement" @ owned) -> bool: return self.read() diff --git a/guppylang/src/guppylang/std/quantum/__init__.py b/guppylang/src/guppylang/std/quantum/__init__.py index 0c78b305b..38599c832 100644 --- a/guppylang/src/guppylang/std/quantum/__init__.py +++ b/guppylang/src/guppylang/std/quantum/__init__.py @@ -26,12 +26,12 @@ class qubit: @no_type_check def __new__() -> "qubit": ... - @guppy # not pure: this is measure+free + @guppy @no_type_check def measure(self: "qubit" @ owned) -> bool: return measure(self) - @guppy(effects=[]) + @guppy @no_type_check def project_z(self: "qubit") -> bool: return project_z(self) @@ -49,7 +49,7 @@ def maybe_qubit() -> Option[qubit]: if allocation succeeds or `nothing` if it fails.""" -@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def h(q: qubit) -> None: r"""Hadamard gate command @@ -63,7 +63,7 @@ def h(q: qubit) -> None: """ -@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cz(control: qubit, target: qubit) -> None: r"""Controlled-Z gate command. @@ -83,7 +83,7 @@ def cz(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cy(control: qubit, target: qubit) -> None: r"""Controlled-Y gate command. @@ -103,7 +103,7 @@ def cy(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cx(control: qubit, target: qubit) -> None: r"""Controlled-X gate command. @@ -123,7 +123,7 @@ def cx(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def t(q: qubit) -> None: r"""T gate. @@ -138,7 +138,7 @@ def t(q: qubit) -> None: """ -@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def s(q: qubit) -> None: r"""S gate. @@ -153,7 +153,7 @@ def s(q: qubit) -> None: """ -@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def v(q: qubit) -> None: r"""V gate. @@ -168,7 +168,7 @@ def v(q: qubit) -> None: """ -@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def x(q: qubit) -> None: r"""X gate. @@ -183,7 +183,7 @@ def x(q: qubit) -> None: """ -@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def y(q: qubit) -> None: r"""Y gate. @@ -198,7 +198,7 @@ def y(q: qubit) -> None: """ -@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def z(q: qubit) -> None: r"""Z gate. @@ -213,7 +213,7 @@ def z(q: qubit) -> None: """ -@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def tdg(q: qubit) -> None: r"""Tdg gate. @@ -228,7 +228,7 @@ def tdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def sdg(q: qubit) -> None: r"""Sdg gate. @@ -243,7 +243,7 @@ def sdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def vdg(q: qubit) -> None: r"""Vdg gate. @@ -258,7 +258,7 @@ def vdg(q: qubit) -> None: """ -@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def rz(q: qubit, angle: angle) -> None: r"""Rz gate. @@ -274,7 +274,7 @@ def rz(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def rx(q: qubit, angle: angle) -> None: r"""Rx gate. @@ -289,7 +289,7 @@ def rx(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def ry(q: qubit, angle: angle) -> None: r"""Ry gate. @@ -304,9 +304,7 @@ def ry(q: qubit, angle: angle) -> None: """ -@custom_function( - RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, effects=[] -) +@custom_function(RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def crz(control: qubit, target: qubit, angle: angle) -> None: r"""Controlled-Rz gate command. @@ -326,7 +324,7 @@ def crz(control: qubit, target: qubit, angle: angle) -> None: """ -@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: r"""A Toffoli gate command. Also sometimes known as a CCX gate. @@ -350,7 +348,7 @@ def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: """ -@custom_function(InoutMeasureCompiler(), effects=[]) +@custom_function(InoutMeasureCompiler()) @no_type_check def project_z(q: qubit) -> bool: """Project a single qubit into the Z-basis (a non-destructive measurement).""" @@ -368,7 +366,7 @@ def measure(q: qubit @ owned) -> bool: """Measure a single qubit destructively.""" -@hugr_op(quantum_op("Reset"), effects=[]) +@hugr_op(quantum_op("Reset")) @no_type_check def reset(q: qubit) -> None: """Reset a single qubit to the :math:`|0\rangle` state.""" @@ -377,7 +375,7 @@ def reset(q: qubit) -> None: N = guppy.nat_var("N") -@guppy # This does N calls to QFree, so it is not pure +@guppy @no_type_check def measure_array(qubits: array[qubit, N] @ owned) -> array[bool, N]: """Measure an array of qubits, returning an array of bools.""" @@ -397,7 +395,7 @@ def discard_array(qubits: array[qubit, N] @ owned) -> None: # -------NON-PRIMITIVE------- -@guppy(effects=[]) +@guppy @no_type_check def ch(control: qubit, target: qubit) -> None: r"""Controlled-H gate command. diff --git a/guppylang/src/guppylang/std/quantum/functional.py b/guppylang/src/guppylang/std/quantum/functional.py index 756348b1e..58cebf98b 100644 --- a/guppylang/src/guppylang/std/quantum/functional.py +++ b/guppylang/src/guppylang/std/quantum/functional.py @@ -15,7 +15,7 @@ from guppylang.std.quantum import qubit -@guppy(effects=[]) +@guppy @no_type_check def h(q: qubit @ owned) -> qubit: """Functional Hadamard gate command.""" @@ -23,7 +23,7 @@ def h(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CZ gate command.""" @@ -31,7 +31,7 @@ def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CX gate command.""" @@ -39,7 +39,7 @@ def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CY gate command.""" @@ -47,7 +47,7 @@ def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def t(q: qubit @ owned) -> qubit: """Functional T gate command.""" @@ -55,7 +55,7 @@ def t(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def s(q: qubit @ owned) -> qubit: """Functional S gate command.""" @@ -63,7 +63,7 @@ def s(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def v(q: qubit @ owned) -> qubit: """Functional V gate command.""" @@ -71,7 +71,7 @@ def v(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def x(q: qubit @ owned) -> qubit: """Functional X gate command.""" @@ -79,7 +79,7 @@ def x(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def y(q: qubit @ owned) -> qubit: """Functional Y gate command.""" @@ -87,7 +87,7 @@ def y(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def z(q: qubit @ owned) -> qubit: """Functional Z gate command.""" @@ -95,7 +95,7 @@ def z(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def tdg(q: qubit @ owned) -> qubit: """Functional Tdg gate command.""" @@ -103,7 +103,7 @@ def tdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def sdg(q: qubit @ owned) -> qubit: """Functional Sdg gate command.""" @@ -111,7 +111,7 @@ def sdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def vdg(q: qubit @ owned) -> qubit: """Functional Vdg gate command.""" @@ -119,7 +119,7 @@ def vdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def rz(q: qubit @ owned, angle: angle) -> qubit: """Functional Rz gate command.""" @@ -127,7 +127,7 @@ def rz(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def rx(q: qubit @ owned, angle: angle) -> qubit: """Functional Rx gate command.""" @@ -135,7 +135,7 @@ def rx(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def ry(q: qubit @ owned, angle: angle) -> qubit: """Functional Ry gate command.""" @@ -143,7 +143,7 @@ def ry(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def crz( control: qubit @ owned, target: qubit @ owned, angle: angle @@ -153,7 +153,7 @@ def crz( return control, target -@guppy(effects=[]) +@guppy @no_type_check def toffoli( control1: qubit @ owned, control2: qubit @ owned, target: qubit @ owned @@ -163,7 +163,7 @@ def toffoli( return control1, control2, target -@guppy(effects=[]) +@guppy @no_type_check def reset(q: qubit @ owned) -> qubit: """Functional Reset command.""" @@ -171,7 +171,7 @@ def reset(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def project_z(q: qubit @ owned) -> tuple[qubit, bool]: """Functional project_z command.""" @@ -182,7 +182,7 @@ def project_z(q: qubit @ owned) -> tuple[qubit, bool]: # -------NON-PRIMITIVE------- -@guppy(effects=[]) +@guppy @no_type_check def ch(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional Controlled-H gate command.""" diff --git a/tests/error/modifier_errors/higher_order.err b/tests/error/modifier_errors/higher_order.err index d9e326b4d..c472124ba 100644 --- a/tests/error/modifier_errors/higher_order.err +++ b/tests/error/modifier_errors/higher_order.err @@ -1,8 +1,8 @@ -Error: Dagger constraint violation (at $FILE:11:4) +Error: Dagger constraint violation (at $FILE:10:4) | - 9 | @guppy(dagger=True) -10 | def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: -11 | f(q) + 8 | @guppy(dagger=True) + 9 | def test_ho(f: Callable[[qubit], None], q: qubit) -> None: +10 | f(q) | ^^^^ This function cannot be called in a dagger context Guppy compilation failed due to 1 previous error diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index a36e8244a..e32fc884c 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -1,13 +1,12 @@ from guppylang.decorator import guppy from guppylang.std.builtins import dagger -from guppylang.std.effects import effects from guppylang.std.quantum import qubit, h, discard from collections.abc import Callable # f would need a flag to be used in dagger context, but no way to specify that yet @guppy(dagger=True) -def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: +def test_ho(f: Callable[[qubit], None], q: qubit) -> None: f(q) diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index 598f67a4c..d78004dcb 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -10,7 +10,7 @@ # Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly # declare the effects of `f` @guppy.declare -def foo(x: Callable[[T], T] @ effects()) -> None: ... +def foo(x: Callable[[T], T]) -> None: ... @guppy diff --git a/tests/error/struct_errors/constructor_missing_arg.err b/tests/error/struct_errors/constructor_missing_arg.err index 9695292e0..1a673d9b8 100644 --- a/tests/error/struct_errors/constructor_missing_arg.err +++ b/tests/error/struct_errors/constructor_missing_arg.err @@ -5,6 +5,6 @@ Error: Not enough arguments (at $FILE:11:12) 11 | MyStruct() | ^^ Missing argument (expected 1, got 0) -Note: Function signature is `int -[]-> MyStruct` +Note: Function signature is `int -> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_too_many_args.err b/tests/error/struct_errors/constructor_too_many_args.err index e993ba76b..2e5f65d47 100644 --- a/tests/error/struct_errors/constructor_too_many_args.err +++ b/tests/error/struct_errors/constructor_too_many_args.err @@ -5,6 +5,6 @@ Error: Too many arguments (at $FILE:11:16) 11 | MyStruct(1, 2, 3) | ^^^^ Unexpected arguments (expected 1, got 3) -Note: Function signature is `int -[]-> MyStruct` +Note: Function signature is `int -> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_left.err b/tests/error/type_errors/and_not_bool_left.err index 01e5c670d..cc8839a67 100644 --- a/tests/error/type_errors/and_not_bool_left.err +++ b/tests/error/type_errors/and_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_right.err b/tests/error/type_errors/and_not_bool_right.err index 9af5981d3..52586da05 100644 --- a/tests/error/type_errors/and_not_bool_right.err +++ b/tests/error/type_errors/and_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:17) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_expr_not_bool.err b/tests/error/type_errors/if_expr_not_bool.err index 6b6dbdfe2..ffb3fb0f9 100644 --- a/tests/error/type_errors/if_expr_not_bool.err +++ b/tests/error/type_errors/if_expr_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return 1 if x else 0 | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_not_bool.err b/tests/error/type_errors/if_not_bool.err index 96ff71a86..b52e7b368 100644 --- a/tests/error/type_errors/if_not_bool.err +++ b/tests/error/type_errors/if_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:7) 7 | if x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_not_bool.err b/tests/error/type_errors/not_not_bool.err index 8d9ad86b2..ab16adf98 100644 --- a/tests/error/type_errors/not_not_bool.err +++ b/tests/error/type_errors/not_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:15) 7 | return not x | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_left.err b/tests/error/type_errors/or_not_bool_left.err index bd8ce6c86..c17142398 100644 --- a/tests/error/type_errors/or_not_bool_left.err +++ b/tests/error/type_errors/or_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_right.err b/tests/error/type_errors/or_not_bool_right.err index f4495645b..4769de617 100644 --- a/tests/error/type_errors/or_not_bool_right.err +++ b/tests/error/type_errors/or_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/while_not_bool.err b/tests/error/type_errors/while_not_bool.err index 1dc04396f..13469fd99 100644 --- a/tests/error/type_errors/while_not_bool.err +++ b/tests/error/type_errors/while_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:10) 7 | while x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/integration/notebooks/misc_notebook_tests.ipynb b/tests/integration/notebooks/misc_notebook_tests.ipynb index 518c17ead..588d46b8b 100644 --- a/tests/integration/notebooks/misc_notebook_tests.ipynb +++ b/tests/integration/notebooks/misc_notebook_tests.ipynb @@ -137,7 +137,7 @@ "16 | return MyStruct()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -[]-> MyStruct`\n", + "Note: Function signature is `int -> MyStruct`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] @@ -243,7 +243,7 @@ "16 | return MyEnum.Var()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -[]-> MyEnum`\n", + "Note: Function signature is `int -> MyEnum`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index b5ff65ed5..b302ba621 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -21,7 +21,6 @@ """ from guppylang import guppy -from guppylang.std.effects import effects from tests.util import compile_guppy from typing import Generic @@ -248,7 +247,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: Callable[[int], Enum[int]] @ effects(), x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int]], x: int) -> Enum[int]: return mk_enum(x) @guppy diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index 42c85699d..55616cec6 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -5,8 +5,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from guppylang.std.effects import effects - def test_basic_defs(validate): @guppy.struct @@ -134,12 +132,8 @@ def test_higher_order(validate): class Struct(Generic[T]): x: T - # Pending https://github.com/Quantinuum/guppylang/issues/1760 - # we must explicitly state the effects of `mk_struct` @guppy - def factory( - mk_struct: "Callable[[int], Struct[int]] @ effects()", x: int - ) -> Struct[int]: + def factory(mk_struct: "Callable[[int], Struct[int]]", x: int) -> Struct[int]: return mk_struct(x) @guppy diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index ecc5e8ec0..dd3a16412 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -2,7 +2,6 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.effects import effects def test_create(validate): @@ -108,7 +107,7 @@ class MyEnum: VariantA = {"x": int} @guppy.comptime - def test() -> Callable[[int], MyEnum] @ effects(): + def test() -> Callable[[int], MyEnum]: return MyEnum.VariantA validate(test.compile_function()) diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index 77e1d647f..f9cb545df 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -2,7 +2,6 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.effects import effects def test_create(run_int_fn): @@ -115,7 +114,7 @@ class S: x: int @guppy.comptime - def test() -> Callable[[int], S] @ effects(): + def test() -> Callable[[int], S]: return S validate(test.compile_function()) From 4523d81e0e05a0ad5f63dcee10f7621ffc6e600a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 21:16:26 +0100 Subject: [PATCH 60/81] Redo stdlib changes, enum+struct constructors, bool conversion This reverts commit 41b68c68f112276a24747ccc0e76fd78aa147ae4. --- .../checker/expr_checker.py | 7 +- .../guppylang_internals/definition/enum.py | 1 + .../guppylang_internals/definition/struct.py | 1 + guppylang/src/guppylang/std/angles.py | 18 +- guppylang/src/guppylang/std/num.py | 245 ++++++++++-------- .../src/guppylang/std/qsystem/__init__.py | 5 +- .../src/guppylang/std/quantum/__init__.py | 50 ++-- .../src/guppylang/std/quantum/functional.py | 42 +-- tests/error/modifier_errors/higher_order.err | 8 +- tests/error/modifier_errors/higher_order.py | 3 +- tests/error/poly_errors/non_linear2.py | 2 +- .../struct_errors/constructor_missing_arg.err | 2 +- .../constructor_too_many_args.err | 2 +- tests/error/type_errors/and_not_bool_left.err | 2 +- .../error/type_errors/and_not_bool_right.err | 2 +- tests/error/type_errors/if_expr_not_bool.err | 2 +- tests/error/type_errors/if_not_bool.err | 2 +- tests/error/type_errors/not_not_bool.err | 2 +- tests/error/type_errors/or_not_bool_left.err | 2 +- tests/error/type_errors/or_not_bool_right.err | 2 +- tests/error/type_errors/while_not_bool.err | 2 +- .../notebooks/misc_notebook_tests.ipynb | 4 +- tests/integration/test_enum.py | 3 +- tests/integration/test_struct.py | 8 +- tests/integration/tracing/test_enum.py | 3 +- tests/integration/tracing/test_struct.py | 3 +- 26 files changed, 233 insertions(+), 190 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 1fc07e655..33c693e10 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1517,6 +1517,9 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type return node, node_ty synth = ExprSynthesizer(ctx) exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Inout)], bool_type()) + # 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([]) try: return synth.synthesize_instance_func( node, [], "__bool__", "truthy", exp_sig, True @@ -1525,7 +1528,9 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type if not node_ty.copyable: # Linear types may implement a `__consume_as_bool__` method that consumes # the value, instead of borrowing it. - exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Owned)], bool_type()) + exp_sig = FunctionType( + [FuncInput(node_ty, InputFlags.Owned)], bool_type() + ).with_effects([]) return synth.synthesize_instance_func( node, [], "__consume_as_bool__", "truthy", exp_sig, True ) diff --git a/guppylang-internals/src/guppylang_internals/definition/enum.py b/guppylang-internals/src/guppylang_internals/definition/enum.py index 1bc17cf8b..bcfc20561 100644 --- a/guppylang-internals/src/guppylang_internals/definition/enum.py +++ b/guppylang-internals/src/guppylang_internals/definition/enum.py @@ -306,6 +306,7 @@ def compile(self, wires: list[Wire]) -> list[Wire]: ], output=enum_type, params=self.params, + declared_effects=[], ) constructor_def = CustomFunctionDef( diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index ae753cb3b..ff40842c9 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -225,6 +225,7 @@ def compile(self, args: list[Wire]) -> list[Wire]: defn=self, args=[p.to_bound(i) for i, p in enumerate(self.params)] ), params=self.params, + declared_effects=[], ) constructor_def = CustomFunctionDef( id=DefId.fresh(), diff --git a/guppylang/src/guppylang/std/angles.py b/guppylang/src/guppylang/std/angles.py index d44aea0b0..4b4fb8f5c 100644 --- a/guppylang/src/guppylang/std/angles.py +++ b/guppylang/src/guppylang/std/angles.py @@ -31,47 +31,47 @@ class angle: halfturns: float - @guppy + @guppy(effects=[]) @no_type_check def __add__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns + other.halfturns) - @guppy + @guppy(effects=[]) @no_type_check def __sub__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns - other.halfturns) - @guppy + @guppy(effects=[]) @no_type_check def __mul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy + @guppy(effects=[]) @no_type_check def __rmul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy + @guppy(effects=[]) @no_type_check def __truediv__(self: "angle", other: float) -> "angle": return angle(self.halfturns / other) - @guppy + @guppy(effects=[]) @no_type_check def __rtruediv__(self: "angle", other: float) -> "angle": return angle(other / self.halfturns) - @guppy + @guppy(effects=[]) @no_type_check def __neg__(self: "angle") -> "angle": return angle(-self.halfturns) - @guppy + @guppy(effects=[]) @no_type_check def __float__(self: "angle") -> float: return self.halfturns * py(math.pi) - @guppy + @guppy(effects=[]) @no_type_check def __eq__(self: "angle", other: "angle") -> bool: return self.halfturns == other.halfturns diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 631abd7c7..7b7ebc263 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -20,94 +20,99 @@ ) from guppylang_internals.tys.builtin import float_type_def, int_type_def, nat_type_def -from guppylang import guppy +from guppylang import Effect, guppy @extend_type(nat_type_def) class nat: """A 64-bit unsigned integer.""" - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __abs__(self: nat) -> nat: ... - @hugr_op(int_op("iadd")) + @hugr_op(int_op("iadd"), effects=[]) def __add__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("iand")) + @hugr_op(int_op("iand"), effects=[]) def __and__(self: nat, other: nat) -> nat: ... - @guppy + @guppy(effects=[]) @no_type_check def __bool__(self: nat) -> bool: return self != 0 - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __ceil__(self: nat) -> nat: ... - @hugr_op(int_op("idivmod_u", n_vars=2)) + # Panics if other == 0 + @hugr_op(int_op("idivmod_u", n_vars=2), effects=[Effect.ANY]) def __divmod__(self: nat, other: nat) -> tuple[nat, nat]: ... - @custom_function(BoolOpCompiler(int_op("ieq"))) + @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) def __eq__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION)) + @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) def __float__(self: nat) -> float: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __floor__(self: nat) -> nat: ... - @hugr_op(int_op("idiv_u")) + # Panics if other == 0 + @hugr_op(int_op("idiv_u"), effects=[Effect.ANY]) def __floordiv__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ige_u"))) + @custom_function(BoolOpCompiler(int_op("ige_u")), effects=[]) def __ge__(self: nat, other: nat) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_u"))) + @custom_function(BoolOpCompiler(int_op("igt_u")), effects=[]) def __gt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("iu_to_s")) + @hugr_op(int_op("iu_to_s"), effects=[]) def __int__(self: nat) -> int: ... - @hugr_op(int_op("inot")) + @hugr_op(int_op("inot"), effects=[]) def __invert__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ile_u"))) + @custom_function(BoolOpCompiler(int_op("ile_u")), effects=[]) def __le__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("ishl")) + @hugr_op(int_op("ishl"), effects=[]) def __lshift__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ilt_u"))) + @custom_function(BoolOpCompiler(int_op("ilt_u")), effects=[]) def __lt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("imod_u")) + # Panics if other == 0 + @hugr_op(int_op("imod_u"), effects=[Effect.ANY]) def __mod__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("imul")) + @hugr_op(int_op("imul"), effects=[]) def __mul__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __nat__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine"))) + @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) def __ne__(self: nat, other: nat) -> bool: ... - @custom_function(checker=DunderChecker("__nat__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__nat__"), higher_order_value=False, effects=[] + ) def __new__(x): ... - @hugr_op(int_op("ior")) + @hugr_op(int_op("ior"), effects=[]) def __or__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: nat) -> nat: ... - @hugr_op(int_op("ipow")) + @hugr_op(int_op("ipow"), effects=[]) def __pow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rand__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) @@ -116,40 +121,40 @@ def __rdivmod__(self: nat, other: nat) -> tuple[nat, nat]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rlshift__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __ror__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __round__(self: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rpow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rrshift__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("ishr")) + @hugr_op(int_op("ishr"), effects=[]) def __rshift__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rtruediv__(self: nat, other: nat) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rxor__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("isub")) + @hugr_op(int_op("isub"), effects=[]) def __sub__(self: nat, other: nat) -> nat: ... @guppy @@ -157,10 +162,10 @@ def __sub__(self: nat, other: nat) -> nat: ... def __truediv__(self: nat, other: nat) -> float: return float(self) / float(other) - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __trunc__(self: nat) -> nat: ... - @hugr_op(int_op("ixor")) + @hugr_op(int_op("ixor"), effects=[]) def __xor__(self: nat, other: nat) -> nat: ... @@ -168,51 +173,56 @@ def __xor__(self: nat, other: nat) -> nat: ... class int: """A 64-bit signed integer.""" - @hugr_op(int_op("iabs")) # TODO: Maybe wrong? (signed vs unsigned!) + @hugr_op( + int_op("iabs"), # TODO: Maybe wrong? (signed vs unsigned!) + effects=[], + ) def __abs__(self: int) -> int: ... - @hugr_op(int_op("iadd")) + @hugr_op(int_op("iadd"), effects=[]) def __add__(self: int, other: int) -> int: ... - @hugr_op(int_op("iand")) + @hugr_op(int_op("iand"), effects=[]) def __and__(self: int, other: int) -> int: ... - @guppy + @guppy(effects=[]) @no_type_check def __bool__(self: int) -> bool: return self != 0 - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __ceil__(self: int) -> int: ... - @hugr_op(int_op("idivmod_s")) + # Panics if other == 0 + @hugr_op(int_op("idivmod_s"), effects=[Effect.ANY]) def __divmod__(self: int, other: int) -> tuple[int, int]: ... - @custom_function(BoolOpCompiler(int_op("ieq"))) + @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) def __eq__(self: int, other: int) -> bool: ... - @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION)) + @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) def __float__(self: int) -> float: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __floor__(self: int) -> int: ... - @hugr_op(int_op("idiv_s")) + # Panics if other == 0 + @hugr_op(int_op("idiv_s"), effects=[Effect.ANY]) def __floordiv__(self: int, other: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ige_s"))) + @custom_function(BoolOpCompiler(int_op("ige_s")), effects=[]) def __ge__(self: int, other: int) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_s"))) + @custom_function(BoolOpCompiler(int_op("igt_s")), effects=[]) def __gt__(self: int, other: int) -> bool: ... @custom_function(NoopCompiler()) def __int__(self: int) -> int: ... - @hugr_op(int_op("inot")) + @hugr_op(int_op("inot"), effects=[]) def __invert__(self: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ile_s"))) + @custom_function(BoolOpCompiler(int_op("ile_s")), effects=[]) def __le__(self: int, other: int) -> bool: ... @hugr_op(int_op("ishl")) # TODO: RHS is unsigned @@ -221,28 +231,30 @@ def __lshift__(self: int, other: int) -> int: ... @custom_function(BoolOpCompiler(int_op("ilt_s"))) def __lt__(self: int, other: int) -> bool: ... - @hugr_op(int_op("imod_s")) + @hugr_op(int_op("imod_s"), effects=[]) def __mod__(self: int, other: int) -> int: ... - @hugr_op(int_op("imul")) + @hugr_op(int_op("imul"), effects=[]) def __mul__(self: int, other: int) -> int: ... - @hugr_op(int_op("is_to_u")) + @hugr_op(int_op("is_to_u"), effects=[]) def __nat__(self: int) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine"))) + @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) def __ne__(self: int, other: int) -> bool: ... - @hugr_op(int_op("ineg")) + @hugr_op(int_op("ineg"), effects=[]) def __neg__(self: int) -> int: ... - @custom_function(checker=DunderChecker("__int__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__int__"), higher_order_value=False, effects=[] + ) def __new__(x): ... - @hugr_op(int_op("ior")) + @hugr_op(int_op("ior"), effects=[]) def __or__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: int) -> int: ... @guppy @@ -255,13 +267,13 @@ def __pow__(self: int, exponent: int) -> int: ) return self.__pow_impl(exponent) - @hugr_op(int_op("ipow")) + @hugr_op(int_op("ipow"), effects=[]) # Exponent is treated as unsigned def __pow_impl(self: int, exponent: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rand__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -270,19 +282,23 @@ def __rdivmod__(self: int, other: int) -> tuple[int, int]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned + @custom_function( + checker=ReversingChecker(), # TODO: RHS is unsigned + effects=[], + ) def __rlshift__(self: int, other: int) -> int: ... + # What if other==0 ? @custom_function(checker=ReversingChecker()) def __rmod__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __ror__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __round__(self: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -291,30 +307,33 @@ def __rpow__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rrshift__(self: int, other: int) -> int: ... - @hugr_op(int_op("ishr")) # TODO: RHS is unsigned + @hugr_op( + int_op("ishr"), # TODO: RHS is unsigned + effects=[], + ) def __rshift__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rtruediv__(self: int, other: int) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rxor__(self: int, other: int) -> int: ... - @hugr_op(int_op("isub")) + @hugr_op(int_op("isub"), effects=[]) def __sub__(self: int, other: int) -> int: ... - @guppy + @guppy(effects=[]) @no_type_check def __truediv__(self: int, other: int) -> float: return float(self) / float(other) - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __trunc__(self: int) -> int: ... - @hugr_op(int_op("ixor")) + @hugr_op(int_op("ixor"), effects=[]) def __xor__(self: int, other: int) -> int: ... @@ -322,124 +341,128 @@ def __xor__(self: int, other: int) -> int: ... class float: """An IEEE754 double-precision floating point value.""" - @hugr_op(float_op("fabs")) + @hugr_op(float_op("fabs"), effects=[]) def __abs__(self: float) -> float: ... - @hugr_op(float_op("fadd")) + @hugr_op(float_op("fadd"), effects=[]) def __add__(self: float, other: float) -> float: ... - @guppy + @guppy(effects=[]) @no_type_check def __bool__(self: float) -> bool: return self != 0.0 - @hugr_op(float_op("fceil")) + @hugr_op(float_op("fceil"), effects=[]) def __ceil__(self: float) -> float: ... - @guppy + @guppy(effects=[]) @no_type_check def __divmod__(self: float, other: float) -> tuple[float, float]: return self // other, self.__mod__(other) - @custom_function(BoolOpCompiler(float_op("feq"))) + @custom_function(BoolOpCompiler(float_op("feq")), effects=[]) def __eq__(self: float, other: float) -> bool: ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __float__(self: float) -> float: ... - @hugr_op(float_op("ffloor")) + @hugr_op(float_op("ffloor"), effects=[]) def __floor__(self: float) -> float: ... - @guppy + @guppy(effects=[]) @no_type_check def __floordiv__(self: float, other: float) -> float: return (self / other).__floor__() - @custom_function(BoolOpCompiler(float_op("fge"))) + @custom_function(BoolOpCompiler(float_op("fge")), effects=[]) def __ge__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("fgt"))) + @custom_function(BoolOpCompiler(float_op("fgt")), effects=[]) def __gt__(self: float, other: float) -> bool: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_s", hugr.std.int.CONVERSIONS_EXTENSION), - ) + ), + effects=[], ) def __int__(self: float) -> int: ... - @custom_function(BoolOpCompiler(float_op("fle"))) + @custom_function(BoolOpCompiler(float_op("fle")), effects=[]) def __le__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("flt"))) + @custom_function(BoolOpCompiler(float_op("flt")), effects=[]) def __lt__(self: float, other: float) -> bool: ... - @guppy + @guppy(effects=[]) @no_type_check def __mod__(self: float, other: float) -> float: return self - (self // other) * other - @hugr_op(float_op("fmul")) + @hugr_op(float_op("fmul"), effects=[]) def __mul__(self: float, other: float) -> float: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_u", hugr.std.int.CONVERSIONS_EXTENSION), - ) + ), + effects=[], ) def __nat__(self: float) -> nat: ... - @custom_function(BoolOpCompiler(float_op("fne"))) + @custom_function(BoolOpCompiler(float_op("fne")), effects=[]) def __ne__(self: float, other: float) -> bool: ... - @hugr_op(float_op("fneg")) + @hugr_op(float_op("fneg"), effects=[]) def __neg__(self: float) -> float: ... - @custom_function(checker=DunderChecker("__float__"), higher_order_value=False) + @custom_function( + checker=DunderChecker("__float__"), higher_order_value=False, effects=[] + ) def __new__(x): ... - @custom_function(NoopCompiler()) + @custom_function(NoopCompiler(), effects=[]) def __pos__(self: float) -> float: ... - @hugr_op(float_op("fpow")) # TODO + @hugr_op(float_op("fpow"), effects=[]) # TODO def __pow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __radd__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rdivmod__(self: float, other: float) -> tuple[float, float]: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rfloordiv__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmod__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rmul__(self: float, other: float) -> float: ... - @hugr_op(float_op("fround")) # TODO + @hugr_op(float_op("fround"), effects=[]) # TODO def __round__(self: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rpow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rsub__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker()) + @custom_function(checker=ReversingChecker(), effects=[]) def __rtruediv__(self: float, other: float) -> float: ... - @hugr_op(float_op("fsub")) + @hugr_op(float_op("fsub"), effects=[]) def __sub__(self: float, other: float) -> float: ... - @hugr_op(float_op("fdiv")) + @hugr_op(float_op("fdiv"), effects=[]) def __truediv__(self: float, other: float) -> float: ... - @hugr_op(unsupported_op("trunc_s")) # TODO `trunc_s` returns an option + @hugr_op(unsupported_op("trunc_s"), effects=[]) # TODO `trunc_s` returns an option def __trunc__(self: float) -> float: ... diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index b2ab03655..e6c0d23f0 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -256,14 +256,15 @@ class Measurement: """Represents the result of a lazy measurement which needs to be explicitly read before being used.""" - @custom_function(compiler=ReadFutureBoolCompiler()) + # We do *not* model the pipeline as a side-effect + @custom_function(compiler=ReadFutureBoolCompiler(), effects=[]) @no_type_check def read(self: "Measurement" @ owned) -> bool: """Read the measurement result, consuming it. Blocks until the result is available if the measurement hasn't been performed yet since being requested. """ - @guppy + @guppy(effects=[]) @no_type_check def __consume_as_bool__(self: "Measurement" @ owned) -> bool: return self.read() diff --git a/guppylang/src/guppylang/std/quantum/__init__.py b/guppylang/src/guppylang/std/quantum/__init__.py index 38599c832..0c78b305b 100644 --- a/guppylang/src/guppylang/std/quantum/__init__.py +++ b/guppylang/src/guppylang/std/quantum/__init__.py @@ -26,12 +26,12 @@ class qubit: @no_type_check def __new__() -> "qubit": ... - @guppy + @guppy # not pure: this is measure+free @no_type_check def measure(self: "qubit" @ owned) -> bool: return measure(self) - @guppy + @guppy(effects=[]) @no_type_check def project_z(self: "qubit") -> bool: return project_z(self) @@ -49,7 +49,7 @@ def maybe_qubit() -> Option[qubit]: if allocation succeeds or `nothing` if it fails.""" -@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def h(q: qubit) -> None: r"""Hadamard gate command @@ -63,7 +63,7 @@ def h(q: qubit) -> None: """ -@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cz(control: qubit, target: qubit) -> None: r"""Controlled-Z gate command. @@ -83,7 +83,7 @@ def cz(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cy(control: qubit, target: qubit) -> None: r"""Controlled-Y gate command. @@ -103,7 +103,7 @@ def cy(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def cx(control: qubit, target: qubit) -> None: r"""Controlled-X gate command. @@ -123,7 +123,7 @@ def cx(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def t(q: qubit) -> None: r"""T gate. @@ -138,7 +138,7 @@ def t(q: qubit) -> None: """ -@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def s(q: qubit) -> None: r"""S gate. @@ -153,7 +153,7 @@ def s(q: qubit) -> None: """ -@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def v(q: qubit) -> None: r"""V gate. @@ -168,7 +168,7 @@ def v(q: qubit) -> None: """ -@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def x(q: qubit) -> None: r"""X gate. @@ -183,7 +183,7 @@ def x(q: qubit) -> None: """ -@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def y(q: qubit) -> None: r"""Y gate. @@ -198,7 +198,7 @@ def y(q: qubit) -> None: """ -@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def z(q: qubit) -> None: r"""Z gate. @@ -213,7 +213,7 @@ def z(q: qubit) -> None: """ -@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def tdg(q: qubit) -> None: r"""Tdg gate. @@ -228,7 +228,7 @@ def tdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def sdg(q: qubit) -> None: r"""Sdg gate. @@ -243,7 +243,7 @@ def sdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def vdg(q: qubit) -> None: r"""Vdg gate. @@ -258,7 +258,7 @@ def vdg(q: qubit) -> None: """ -@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary) +@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def rz(q: qubit, angle: angle) -> None: r"""Rz gate. @@ -274,7 +274,7 @@ def rz(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary) +@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def rx(q: qubit, angle: angle) -> None: r"""Rx gate. @@ -289,7 +289,7 @@ def rx(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary) +@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def ry(q: qubit, angle: angle) -> None: r"""Ry gate. @@ -304,7 +304,9 @@ def ry(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary) +@custom_function( + RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, effects=[] +) @no_type_check def crz(control: qubit, target: qubit, angle: angle) -> None: r"""Controlled-Rz gate command. @@ -324,7 +326,7 @@ def crz(control: qubit, target: qubit, angle: angle) -> None: """ -@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary) +@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, effects=[]) @no_type_check def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: r"""A Toffoli gate command. Also sometimes known as a CCX gate. @@ -348,7 +350,7 @@ def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: """ -@custom_function(InoutMeasureCompiler()) +@custom_function(InoutMeasureCompiler(), effects=[]) @no_type_check def project_z(q: qubit) -> bool: """Project a single qubit into the Z-basis (a non-destructive measurement).""" @@ -366,7 +368,7 @@ def measure(q: qubit @ owned) -> bool: """Measure a single qubit destructively.""" -@hugr_op(quantum_op("Reset")) +@hugr_op(quantum_op("Reset"), effects=[]) @no_type_check def reset(q: qubit) -> None: """Reset a single qubit to the :math:`|0\rangle` state.""" @@ -375,7 +377,7 @@ def reset(q: qubit) -> None: N = guppy.nat_var("N") -@guppy +@guppy # This does N calls to QFree, so it is not pure @no_type_check def measure_array(qubits: array[qubit, N] @ owned) -> array[bool, N]: """Measure an array of qubits, returning an array of bools.""" @@ -395,7 +397,7 @@ def discard_array(qubits: array[qubit, N] @ owned) -> None: # -------NON-PRIMITIVE------- -@guppy +@guppy(effects=[]) @no_type_check def ch(control: qubit, target: qubit) -> None: r"""Controlled-H gate command. diff --git a/guppylang/src/guppylang/std/quantum/functional.py b/guppylang/src/guppylang/std/quantum/functional.py index 58cebf98b..756348b1e 100644 --- a/guppylang/src/guppylang/std/quantum/functional.py +++ b/guppylang/src/guppylang/std/quantum/functional.py @@ -15,7 +15,7 @@ from guppylang.std.quantum import qubit -@guppy +@guppy(effects=[]) @no_type_check def h(q: qubit @ owned) -> qubit: """Functional Hadamard gate command.""" @@ -23,7 +23,7 @@ def h(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CZ gate command.""" @@ -31,7 +31,7 @@ def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(effects=[]) @no_type_check def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CX gate command.""" @@ -39,7 +39,7 @@ def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(effects=[]) @no_type_check def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CY gate command.""" @@ -47,7 +47,7 @@ def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy +@guppy(effects=[]) @no_type_check def t(q: qubit @ owned) -> qubit: """Functional T gate command.""" @@ -55,7 +55,7 @@ def t(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def s(q: qubit @ owned) -> qubit: """Functional S gate command.""" @@ -63,7 +63,7 @@ def s(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def v(q: qubit @ owned) -> qubit: """Functional V gate command.""" @@ -71,7 +71,7 @@ def v(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def x(q: qubit @ owned) -> qubit: """Functional X gate command.""" @@ -79,7 +79,7 @@ def x(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def y(q: qubit @ owned) -> qubit: """Functional Y gate command.""" @@ -87,7 +87,7 @@ def y(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def z(q: qubit @ owned) -> qubit: """Functional Z gate command.""" @@ -95,7 +95,7 @@ def z(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def tdg(q: qubit @ owned) -> qubit: """Functional Tdg gate command.""" @@ -103,7 +103,7 @@ def tdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def sdg(q: qubit @ owned) -> qubit: """Functional Sdg gate command.""" @@ -111,7 +111,7 @@ def sdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def vdg(q: qubit @ owned) -> qubit: """Functional Vdg gate command.""" @@ -119,7 +119,7 @@ def vdg(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def rz(q: qubit @ owned, angle: angle) -> qubit: """Functional Rz gate command.""" @@ -127,7 +127,7 @@ def rz(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def rx(q: qubit @ owned, angle: angle) -> qubit: """Functional Rx gate command.""" @@ -135,7 +135,7 @@ def rx(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def ry(q: qubit @ owned, angle: angle) -> qubit: """Functional Ry gate command.""" @@ -143,7 +143,7 @@ def ry(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def crz( control: qubit @ owned, target: qubit @ owned, angle: angle @@ -153,7 +153,7 @@ def crz( return control, target -@guppy +@guppy(effects=[]) @no_type_check def toffoli( control1: qubit @ owned, control2: qubit @ owned, target: qubit @ owned @@ -163,7 +163,7 @@ def toffoli( return control1, control2, target -@guppy +@guppy(effects=[]) @no_type_check def reset(q: qubit @ owned) -> qubit: """Functional Reset command.""" @@ -171,7 +171,7 @@ def reset(q: qubit @ owned) -> qubit: return q -@guppy +@guppy(effects=[]) @no_type_check def project_z(q: qubit @ owned) -> tuple[qubit, bool]: """Functional project_z command.""" @@ -182,7 +182,7 @@ def project_z(q: qubit @ owned) -> tuple[qubit, bool]: # -------NON-PRIMITIVE------- -@guppy +@guppy(effects=[]) @no_type_check def ch(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional Controlled-H gate command.""" diff --git a/tests/error/modifier_errors/higher_order.err b/tests/error/modifier_errors/higher_order.err index c472124ba..d9e326b4d 100644 --- a/tests/error/modifier_errors/higher_order.err +++ b/tests/error/modifier_errors/higher_order.err @@ -1,8 +1,8 @@ -Error: Dagger constraint violation (at $FILE:10:4) +Error: Dagger constraint violation (at $FILE:11:4) | - 8 | @guppy(dagger=True) - 9 | def test_ho(f: Callable[[qubit], None], q: qubit) -> None: -10 | f(q) + 9 | @guppy(dagger=True) +10 | def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: +11 | f(q) | ^^^^ This function cannot be called in a dagger context Guppy compilation failed due to 1 previous error diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index e32fc884c..a36e8244a 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -1,12 +1,13 @@ from guppylang.decorator import guppy from guppylang.std.builtins import dagger +from guppylang.std.effects import effects from guppylang.std.quantum import qubit, h, discard from collections.abc import Callable # f would need a flag to be used in dagger context, but no way to specify that yet @guppy(dagger=True) -def test_ho(f: Callable[[qubit], None], q: qubit) -> None: +def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: f(q) diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index d78004dcb..598f67a4c 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -10,7 +10,7 @@ # Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly # declare the effects of `f` @guppy.declare -def foo(x: Callable[[T], T]) -> None: ... +def foo(x: Callable[[T], T] @ effects()) -> None: ... @guppy diff --git a/tests/error/struct_errors/constructor_missing_arg.err b/tests/error/struct_errors/constructor_missing_arg.err index 1a673d9b8..9695292e0 100644 --- a/tests/error/struct_errors/constructor_missing_arg.err +++ b/tests/error/struct_errors/constructor_missing_arg.err @@ -5,6 +5,6 @@ Error: Not enough arguments (at $FILE:11:12) 11 | MyStruct() | ^^ Missing argument (expected 1, got 0) -Note: Function signature is `int -> MyStruct` +Note: Function signature is `int -[]-> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_too_many_args.err b/tests/error/struct_errors/constructor_too_many_args.err index 2e5f65d47..e993ba76b 100644 --- a/tests/error/struct_errors/constructor_too_many_args.err +++ b/tests/error/struct_errors/constructor_too_many_args.err @@ -5,6 +5,6 @@ Error: Too many arguments (at $FILE:11:16) 11 | MyStruct(1, 2, 3) | ^^^^ Unexpected arguments (expected 1, got 3) -Note: Function signature is `int -> MyStruct` +Note: Function signature is `int -[]-> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_left.err b/tests/error/type_errors/and_not_bool_left.err index cc8839a67..01e5c670d 100644 --- a/tests/error/type_errors/and_not_bool_left.err +++ b/tests/error/type_errors/and_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_right.err b/tests/error/type_errors/and_not_bool_right.err index 52586da05..9af5981d3 100644 --- a/tests/error/type_errors/and_not_bool_right.err +++ b/tests/error/type_errors/and_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:17) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_expr_not_bool.err b/tests/error/type_errors/if_expr_not_bool.err index ffb3fb0f9..6b6dbdfe2 100644 --- a/tests/error/type_errors/if_expr_not_bool.err +++ b/tests/error/type_errors/if_expr_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return 1 if x else 0 | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_not_bool.err b/tests/error/type_errors/if_not_bool.err index b52e7b368..96ff71a86 100644 --- a/tests/error/type_errors/if_not_bool.err +++ b/tests/error/type_errors/if_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:7) 7 | if x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_not_bool.err b/tests/error/type_errors/not_not_bool.err index ab16adf98..8d9ad86b2 100644 --- a/tests/error/type_errors/not_not_bool.err +++ b/tests/error/type_errors/not_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:15) 7 | return not x | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_left.err b/tests/error/type_errors/or_not_bool_left.err index c17142398..bd8ce6c86 100644 --- a/tests/error/type_errors/or_not_bool_left.err +++ b/tests/error/type_errors/or_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_right.err b/tests/error/type_errors/or_not_bool_right.err index 4769de617..f4495645b 100644 --- a/tests/error/type_errors/or_not_bool_right.err +++ b/tests/error/type_errors/or_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/while_not_bool.err b/tests/error/type_errors/while_not_bool.err index 13469fd99..1dc04396f 100644 --- a/tests/error/type_errors/while_not_bool.err +++ b/tests/error/type_errors/while_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:10) 7 | while x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -> bool` +Help: Implement missing method: `__bool__: NonBool -[]-> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/integration/notebooks/misc_notebook_tests.ipynb b/tests/integration/notebooks/misc_notebook_tests.ipynb index 588d46b8b..518c17ead 100644 --- a/tests/integration/notebooks/misc_notebook_tests.ipynb +++ b/tests/integration/notebooks/misc_notebook_tests.ipynb @@ -137,7 +137,7 @@ "16 | return MyStruct()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -> MyStruct`\n", + "Note: Function signature is `int -[]-> MyStruct`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] @@ -243,7 +243,7 @@ "16 | return MyEnum.Var()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -> MyEnum`\n", + "Note: Function signature is `int -[]-> MyEnum`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index b302ba621..b5ff65ed5 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -21,6 +21,7 @@ """ from guppylang import guppy +from guppylang.std.effects import effects from tests.util import compile_guppy from typing import Generic @@ -247,7 +248,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: Callable[[int], Enum[int]], x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int]] @ effects(), x: int) -> Enum[int]: return mk_enum(x) @guppy diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index 55616cec6..42c85699d 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -5,6 +5,8 @@ if TYPE_CHECKING: from collections.abc import Callable + from guppylang.std.effects import effects + def test_basic_defs(validate): @guppy.struct @@ -132,8 +134,12 @@ def test_higher_order(validate): class Struct(Generic[T]): x: T + # Pending https://github.com/Quantinuum/guppylang/issues/1760 + # we must explicitly state the effects of `mk_struct` @guppy - def factory(mk_struct: "Callable[[int], Struct[int]]", x: int) -> Struct[int]: + def factory( + mk_struct: "Callable[[int], Struct[int]] @ effects()", x: int + ) -> Struct[int]: return mk_struct(x) @guppy diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index dd3a16412..ecc5e8ec0 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -2,6 +2,7 @@ from typing import Generic from guppylang.decorator import guppy +from guppylang.std.effects import effects def test_create(validate): @@ -107,7 +108,7 @@ class MyEnum: VariantA = {"x": int} @guppy.comptime - def test() -> Callable[[int], MyEnum]: + def test() -> Callable[[int], MyEnum] @ effects(): return MyEnum.VariantA validate(test.compile_function()) diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index f9cb545df..77e1d647f 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -2,6 +2,7 @@ from typing import Generic from guppylang.decorator import guppy +from guppylang.std.effects import effects def test_create(run_int_fn): @@ -114,7 +115,7 @@ class S: x: int @guppy.comptime - def test() -> Callable[[int], S]: + def test() -> Callable[[int], S] @ effects(): return S validate(test.compile_function()) From 78f12ed7224e2b6fbe24c62b31f7e52883b3d12c Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 11:22:42 +0100 Subject: [PATCH 61/81] Add test w/custom decorator -> InternalError, oops --- .../pure_calls_impure_custom_def.err | 38 +++++++++++++++++++ .../pure_calls_impure_custom_def.py | 14 +++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/error/effects_errors/pure_calls_impure_custom_def.err create mode 100644 tests/error/effects_errors/pure_calls_impure_custom_def.py diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err new file mode 100644 index 000000000..63614eaab --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -0,0 +1,38 @@ +Traceback (most recent call last): + File "$FILE", line 14, in + main.compile() + File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 205, in compile + return self.compile_entrypoint() + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped + return f(*args, **kwargs) + File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 218, in compile_entrypoint + pack = self.compile_function() + File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 250, in compile_function + return super().compile() + File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 88, in compile + package: Package = ENGINE.compile_single(self.id).package + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped + return f(*args, **kwargs) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 438, in compile_single + pointer, [compiled_def] = self._compile([id]) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 463, in _compile + self.check(def_ids, reset=reset) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped + return f(*args, **kwargs) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 430, in check + self.checked[id, mono_args] = self.get_checked(id, mono_args) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped + return f(*args, **kwargs) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 294, in get_checked + checked_defn = defn.check(mono_args, Globals(DEF_STORE.frames[defn.id])) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/definition/function.py", line 178, in check + cfg = check_global_func_def( + self.defined_at, + ...<2 lines>... + globals, + ) + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/checker/func_checker.py", line 168, in check_global_func_def + raise InternalGuppyError( + ...<2 lines>... + ) +guppylang_internals.error.InternalGuppyError: Effects limited to [] but cannot identify decorator imposing this limit diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.py b/tests/error/effects_errors/pure_calls_impure_custom_def.py new file mode 100644 index 000000000..c0dc31092 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.py @@ -0,0 +1,14 @@ +from guppylang.decorator import guppy + +def custom_pure(func): + return guppy(effects=[])(func) + +@guppy +def impure_func(x: int) -> int: + return x + 1 + +@custom_pure +def main() -> int: + return impure_func(5) + +main.compile() \ No newline at end of file From db750295548a8d5d642fcfa2c2871bc9715809a4 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 11:35:46 +0100 Subject: [PATCH 62/81] refactor: max_effects_from stores Span --- .../src/guppylang_internals/checker/cfg_checker.py | 7 ++++--- .../src/guppylang_internals/checker/core.py | 3 ++- .../src/guppylang_internals/checker/func_checker.py | 5 +++-- .../src/guppylang_internals/checker/modifier_checker.py | 5 +++-- .../src/guppylang_internals/definition/overloaded.py | 2 +- .../error/effects_errors/pure_calls_impure_custom_def.err | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 69f3a0552..18ab148c5 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, field from typing import ClassVar, Generic, TypeVar -from guppylang_internals.ast_util import AstNode, line_col +from guppylang_internals.ast_util import line_col from guppylang_internals.cfg.bb import BB from guppylang_internals.cfg.cfg import CFG, BaseCFG from guppylang_internals.checker.core import ( @@ -25,6 +25,7 @@ from guppylang_internals.checker.stmt_checker import StmtChecker from guppylang_internals.diagnostic import Error, Note from guppylang_internals.error import GuppyError +from guppylang_internals.span import Span from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.ty import InputFlags, Type @@ -77,7 +78,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], Span] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -232,7 +233,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], Span] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index 3b1bc2989..f1a596dbc 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -26,6 +26,7 @@ ) from guppylang_internals.engine import BUILTIN_DEFS, DEF_STORE, ENGINE from guppylang_internals.error import InternalGuppyError, RequiresMonomorphizationError +from guppylang_internals.span import 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 @@ -451,7 +452,7 @@ class Context(NamedTuple): """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: tuple[list[Effect], AstNode] | None = None + max_effects_from: tuple[list[Effect], Span] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 83472a4d6..07707ba78 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -25,6 +25,7 @@ from guppylang_internals.error import GuppyError, InternalGuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef +from guppylang_internals.span import to_span from guppylang_internals.tys import Effect from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.parsing import ( @@ -164,12 +165,12 @@ def check_global_func_def( max_effects_from = None else: dec = _find_guppy_decorator(func_def.decorator_list) - if dec is None and ty.declared_effects is not None: + if dec is None: raise InternalGuppyError( f"Effects limited to {Effect.format_list(ty.declared_effects)}" " but cannot identify decorator imposing this limit" ) - max_effects_from = (ty.declared_effects, dec) + max_effects_from = (ty.declared_effects, to_span(dec)) return check_cfg( cfg, inputs, diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index b137596a5..5dedf01dc 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -2,7 +2,7 @@ import ast -from guppylang_internals.ast_util import AstNode, loop_in_ast, with_loc +from guppylang_internals.ast_util import loop_in_ast, 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 @@ -10,6 +10,7 @@ from guppylang_internals.definition.common import DefId from guppylang_internals.error import GuppyError from guppylang_internals.nodes import CheckedModifiedBlock, ModifiedBlock +from guppylang_internals.span import Span from guppylang_internals.tys import Effect from guppylang_internals.tys.ty import ( FuncInput, @@ -24,7 +25,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects_from: tuple[list[Effect], AstNode] | None, + max_effects_from: tuple[list[Effect], Span] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index 09e30b68f..eb614b464 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -40,7 +40,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None - max_effects_from: tuple[list[Effect], AstNode] | None + max_effects_from: tuple[list[Effect], Span] | None @property def rendered_span_label(self) -> str: diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err index 63614eaab..95b82c287 100644 --- a/tests/error/effects_errors/pure_calls_impure_custom_def.err +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -31,7 +31,7 @@ Traceback (most recent call last): ...<2 lines>... globals, ) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/checker/func_checker.py", line 168, in check_global_func_def + File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/checker/func_checker.py", line 169, in check_global_func_def raise InternalGuppyError( ...<2 lines>... ) From 7b26c25f78c181b2f10d87695b752c301492ece2 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 12:24:14 +0100 Subject: [PATCH 63/81] Give whole FunctionDef as context if can't find decorator...includes body :( --- .../checker/func_checker.py | 13 ++--- .../src/guppylang_internals/span.py | 6 ++ .../pure_calls_impure_custom_def.err | 55 ++++++------------- 3 files changed, 27 insertions(+), 47 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 07707ba78..e37ec1cf8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -22,11 +22,10 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.diagnostic import Error, Help, Note from guppylang_internals.engine import DEF_STORE, ENGINE -from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.error import GuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef from guppylang_internals.span import to_span -from guppylang_internals.tys import Effect from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.parsing import ( TypeParsingCtx, @@ -164,13 +163,9 @@ def check_global_func_def( if ty.declared_effects is None: max_effects_from = None else: - dec = _find_guppy_decorator(func_def.decorator_list) - if dec is None: - raise InternalGuppyError( - f"Effects limited to {Effect.format_list(ty.declared_effects)}" - " but cannot identify decorator imposing this limit" - ) - max_effects_from = (ty.declared_effects, to_span(dec)) + deco = _find_guppy_decorator(func_def.decorator_list) + effects_declared = func_def if deco is None else deco + max_effects_from = (ty.declared_effects, to_span(effects_declared)) return check_cfg( cfg, inputs, diff --git a/guppylang-internals/src/guppylang_internals/span.py b/guppylang-internals/src/guppylang_internals/span.py index b35366d9d..bd27276fc 100644 --- a/guppylang-internals/src/guppylang_internals/span.py +++ b/guppylang-internals/src/guppylang_internals/span.py @@ -80,6 +80,12 @@ def __len__(self) -> int: raise InternalGuppyError("Span: Tried to compute length of multi-line span") return self.end.column - self.start.column + def __bool__(self) -> bool: + """A span is considered false if it has zero length.""" + # Avoid calling __len__: a multi-line span is considered True + # even though its length is not computable + return self.start != self.end + @property def file(self) -> str: """The file containing this span.""" diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err index 95b82c287..852049180 100644 --- a/tests/error/effects_errors/pure_calls_impure_custom_def.err +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -1,38 +1,17 @@ -Traceback (most recent call last): - File "$FILE", line 14, in - main.compile() - File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 205, in compile - return self.compile_entrypoint() - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped - return f(*args, **kwargs) - File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 218, in compile_entrypoint - pack = self.compile_function() - File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 250, in compile_function - return super().compile() - File "/Users/alanlawrence/repos/guppylang/guppylang/src/guppylang/defs.py", line 88, in compile - package: Package = ENGINE.compile_single(self.id).package - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped - return f(*args, **kwargs) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 438, in compile_single - pointer, [compiled_def] = self._compile([id]) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 463, in _compile - self.check(def_ids, reset=reset) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped - return f(*args, **kwargs) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 430, in check - self.checked[id, mono_args] = self.get_checked(id, mono_args) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/error.py", line 131, in pretty_errors_wrapped - return f(*args, **kwargs) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/engine.py", line 294, in get_checked - checked_defn = defn.check(mono_args, Globals(DEF_STORE.frames[defn.id])) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/definition/function.py", line 178, in check - cfg = check_global_func_def( - self.defined_at, - ...<2 lines>... - globals, - ) - File "/Users/alanlawrence/repos/guppylang/guppylang-internals/src/guppylang_internals/checker/func_checker.py", line 169, in check_global_func_def - raise InternalGuppyError( - ...<2 lines>... - ) -guppylang_internals.error.InternalGuppyError: Effects limited to [] but cannot identify decorator imposing this limit +Error: Too many effects (at $FILE:12:10) + | +10 | @custom_pure +11 | def main() -> int: +12 | return impure_func(5) + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed + | those allowed + +Note: + | +10 | @custom_pure +11 | def main() -> int: + | ------------------ +12 | return impure_func(5) + | ------------------------ Allowed effects `[]` declared here + +Guppy compilation failed due to 1 previous error From 63a5b4e60ba04d94144b717ccb6b618c16d7c2fe Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 12:41:45 +0100 Subject: [PATCH 64/81] Include decorators, exclude body --- .../checker/func_checker.py | 23 +++++++++++++++---- .../src/guppylang_internals/span.py | 8 +++++++ .../pure_calls_impure_custom_def.err | 6 ++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index e37ec1cf8..7c034410b 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -9,6 +9,7 @@ import copy import sys from dataclasses import dataclass, replace +from functools import reduce from typing import TYPE_CHECKING, ClassVar, cast from guppylang_internals.ast_util import AstNode, return_nodes_in_ast, with_loc @@ -25,7 +26,7 @@ from guppylang_internals.error import GuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef -from guppylang_internals.span import to_span +from guppylang_internals.span import Span, to_span from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.parsing import ( TypeParsingCtx, @@ -162,10 +163,24 @@ def check_global_func_def( } if ty.declared_effects is None: max_effects_from = None + elif (deco := _find_guppy_decorator(func_def.decorator_list)) is not None: + max_effects_from = (ty.declared_effects, to_span(deco)) else: - deco = _find_guppy_decorator(func_def.decorator_list) - effects_declared = func_def if deco is None else deco - max_effects_from = (ty.declared_effects, to_span(effects_declared)) + # 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 + + max_effects_from = ( + ty.declared_effects, + reduce(union, (to_span(e) for e in elems)), + ) return check_cfg( cfg, inputs, diff --git a/guppylang-internals/src/guppylang_internals/span.py b/guppylang-internals/src/guppylang_internals/span.py index bd27276fc..d9e1d77ba 100644 --- a/guppylang-internals/src/guppylang_internals/span.py +++ b/guppylang-internals/src/guppylang_internals/span.py @@ -71,6 +71,14 @@ def __and__(self, other: "Span") -> "Span | None": return None return Span(max(self.start, other.start), min(self.end, other.end)) + def __or__(self, other: "Span") -> "Span | None": + """Returns the union with the given span, including any gaps, but `None` + if they are in different files.""" + if self.file != other.file: + return None + r = Span(min(self.start, other.start), max(self.end, other.end)) + return r + def __len__(self) -> int: """Returns the length of a single-line span in columns. diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err index 852049180..6181804af 100644 --- a/tests/error/effects_errors/pure_calls_impure_custom_def.err +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -8,10 +8,10 @@ Error: Too many effects (at $FILE:12:10) Note: | + 9 | 10 | @custom_pure + | ----------- 11 | def main() -> int: - | ------------------ -12 | return impure_func(5) - | ------------------------ Allowed effects `[]` declared here + | ----------------- Allowed effects `[]` declared here Guppy compilation failed due to 1 previous error From 8659f75c83ee8d481aa66927946694d3f2c10c23 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 13:06:47 +0100 Subject: [PATCH 65/81] Better errors when parsing @ effects --- .../src/guppylang_internals/tys/errors.py | 22 +++++++++++++ .../src/guppylang_internals/tys/parsing.py | 32 ++++++++++++------- tests/error/effects_errors/effects_on_int.err | 8 +++++ tests/error/effects_errors/effects_on_int.py | 11 +++++++ .../error/effects_errors/misnamed_effects.err | 8 +++++ .../error/effects_errors/misnamed_effects.py | 14 ++++++++ .../error/effects_errors/repeated_effects.err | 8 +++++ .../error/effects_errors/repeated_effects.py | 14 ++++++++ tests/integration/test_effects.py | 2 +- 9 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tests/error/effects_errors/effects_on_int.err create mode 100644 tests/error/effects_errors/effects_on_int.py create mode 100644 tests/error/effects_errors/misnamed_effects.err create mode 100644 tests/error/effects_errors/misnamed_effects.py create mode 100644 tests/error/effects_errors/repeated_effects.err create mode 100644 tests/error/effects_errors/repeated_effects.py diff --git a/guppylang-internals/src/guppylang_internals/tys/errors.py b/guppylang-internals/src/guppylang_internals/tys/errors.py index 3ba951ed1..c6cac17c3 100644 --- a/guppylang-internals/src/guppylang_internals/tys/errors.py +++ b/guppylang-internals/src/guppylang_internals/tys/errors.py @@ -172,6 +172,28 @@ class ComptimeArgShadowError(Error): arg: str +@dataclass(frozen=True) +class EffectsNotApplicableError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = "Effects may be applied only to a Callable type" + + +@dataclass(frozen=True) +class EffectsRepeatedError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = ( + "Effects have already been applied to this Callable type" + ) + + +@dataclass(frozen=True) +class InvalidEffectError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = "Not a valid effect: {arg}" + # We could perhaps provide a list of possible effects? + arg: str + + @dataclass(frozen=True) class InvalidFlagError(Error): title: ClassVar[str] = "Invalid annotation" diff --git a/guppylang-internals/src/guppylang_internals/tys/parsing.py b/guppylang-internals/src/guppylang_internals/tys/parsing.py index f07af59e5..43409a4d6 100644 --- a/guppylang-internals/src/guppylang_internals/tys/parsing.py +++ b/guppylang-internals/src/guppylang_internals/tys/parsing.py @@ -26,11 +26,14 @@ from guppylang_internals.tys.errors import ( CallableComptimeError, ComptimeArgShadowError, + EffectsNotApplicableError, + EffectsRepeatedError, FlagNotAllowedError, FreeTypeVarError, HigherKindedTypeVarError, IllegalComptimeTypeArgError, InvalidCallableTypeError, + InvalidEffectError, InvalidFlagError, InvalidTypeArgError, InvalidTypeError, @@ -473,19 +476,26 @@ def type_with_flags_from_ast( 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)) + raise GuppyError(EffectsNotApplicableError(node.right)) if ty.declared_effects is not None: - raise GuppyError(InvalidFlagError(node.right)) + raise GuppyError(EffectsRepeatedError(node.right)) effects: list[Effect] = [] - for e in fx.args: - # We might want to support ast.Attribute with LHS "Effects" - # and look at RHS - if not isinstance(e, ast.Name): - raise GuppyError(InvalidFlagError(node.right)) - try: - effects.append(Effect.__from_str__(e.id)) - except ValueError: - raise GuppyError(InvalidFlagError(node.right)) # noqa: B904 + if ( + len(fx.args) == 1 + and isinstance(fx.args[0], ast.Constant) + and fx.args[0].value is None + ): + effects = [] + else: + for e in fx.args: + # We might want to support ast.Attribute with LHS "Effects" + # and look at RHS + if not isinstance(e, ast.Name): + raise GuppyError(InvalidEffectError(node.right, str(e))) + try: + effects.append(Effect.__from_str__(e.id)) + except ValueError: + raise GuppyError(InvalidEffectError(node.right, e.id)) # noqa: B904 ty = ty.with_effects(effects) case _: raise GuppyError(InvalidFlagError(node.right)) diff --git a/tests/error/effects_errors/effects_on_int.err b/tests/error/effects_errors/effects_on_int.err new file mode 100644 index 000000000..34789fb1b --- /dev/null +++ b/tests/error/effects_errors/effects_on_int.err @@ -0,0 +1,8 @@ +Error: Invalid annotation (at $FILE:8:26) + | +6 | # This says the return type (not the function) has effects +7 | @guppy +8 | def main(x: int) -> int @ effects(): + | ^^^^^^^^^ Effects may be applied only to a Callable type + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/effects_on_int.py b/tests/error/effects_errors/effects_on_int.py new file mode 100644 index 000000000..1666c4131 --- /dev/null +++ b/tests/error/effects_errors/effects_on_int.py @@ -0,0 +1,11 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy +from guppylang.std.effects import effects + +# This says the return type (not the function) has effects +@guppy +def main(x: int) -> int @ effects(): + return x + 1 + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/misnamed_effects.err b/tests/error/effects_errors/misnamed_effects.err new file mode 100644 index 000000000..d2c20c740 --- /dev/null +++ b/tests/error/effects_errors/misnamed_effects.err @@ -0,0 +1,8 @@ +Error: Invalid annotation (at $FILE:7:36) + | +5 | +6 | @guppy.declare +7 | def foo() -> Callable[[int], int] @ effects(ALL): + | ^^^^^^^^^^^^ Not a valid effect: ALL + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/misnamed_effects.py b/tests/error/effects_errors/misnamed_effects.py new file mode 100644 index 000000000..5f276c33d --- /dev/null +++ b/tests/error/effects_errors/misnamed_effects.py @@ -0,0 +1,14 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy +from guppylang.std.effects import effects, ANY as ALL + +@guppy.declare +def foo() -> Callable[[int], int] @ effects(ALL): + ... + +@guppy +def main() -> int: + return foo()(5) + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/repeated_effects.err b/tests/error/effects_errors/repeated_effects.err new file mode 100644 index 000000000..7847a7aac --- /dev/null +++ b/tests/error/effects_errors/repeated_effects.err @@ -0,0 +1,8 @@ +Error: Invalid annotation (at $FILE:7:48) + | +5 | +6 | @guppy.declare +7 | def foo() -> Callable[[int], int] @ effects() @ effects(ANY): + | ^^^^^^^^^^^^ Effects have already been applied to this Callable type + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/repeated_effects.py b/tests/error/effects_errors/repeated_effects.py new file mode 100644 index 000000000..8ef0708b6 --- /dev/null +++ b/tests/error/effects_errors/repeated_effects.py @@ -0,0 +1,14 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy +from guppylang.std.effects import effects, ANY + +@guppy.declare +def foo() -> Callable[[int], int] @ effects() @ effects(ANY): + ... + +@guppy +def main() -> int: + return foo()(5) + +main.compile() \ No newline at end of file diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index 753c32cff..380d68696 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -127,7 +127,7 @@ def impure_func(pure_f: Callable[[int], int] @ effects()) -> int: def test_pure_callable_from_pure(validate): @guppy(effects=[]) - def pure_func(pure_f: Callable[[int], int] @ effects()) -> int: + def pure_func(pure_f: Callable[[int], int] @ effects(None)) -> int: return pure_f(5) + 1 validate(pure_func.compile_function()) From 31b3a69eaf4289b9cb9ddf566f3294ab69abd189 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 13:13:54 +0100 Subject: [PATCH 66/81] make decorator more flexible --- guppylang/src/guppylang/decorator.py | 22 +++++++++++++++------- tests/integration/test_effects.py | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 3cd4a27f3..638516df9 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -120,7 +120,8 @@ class GuppyKwargs(TypedDict, total=False): power: bool max_qubits: int link_name: str - effects: list[Effect] + # effects=None means no effects, distinct from not specifying effects= at all + effects: list[Effect] | Effect | None class GuppyStructKwargs(TypedDict, total=False): @@ -876,12 +877,19 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: metadata.set_max_qubits(kwargs.pop("max_qubits")) link_name = kwargs.pop("link_name", None) - max_effects_input = kwargs.pop("effects", None) - effects = ( - None - if max_effects_input is None - else [effect.to_internal() for effect in max_effects_input] - ) + + if "effects" in kwargs: + max_effects_input = kwargs.pop("effects") + effects = ( + [] + if max_effects_input is None + else [max_effects_input.to_internal()] + if isinstance(max_effects_input, Effect) + else [effect.to_internal() for effect in max_effects_input] + ) + else: + # Not specified + effects = None if remaining := next(iter(kwargs), None): err = f"Unknown keyword argument: `{remaining}`" diff --git a/tests/integration/test_effects.py b/tests/integration/test_effects.py index 380d68696..6e5962840 100644 --- a/tests/integration/test_effects.py +++ b/tests/integration/test_effects.py @@ -23,7 +23,7 @@ def test_pure_decl_from_explicit_impure(validate): @guppy.declare(effects=[]) def pure_func(x: int) -> int: ... - @guppy(effects=[Effect.ANY]) + @guppy(effects=Effect.ANY) def impure_func(x: int) -> int: return pure_func(x) + 1 @@ -61,7 +61,7 @@ def impure_func2(x: int) -> int: def test_pure_from_impure(validate): - @guppy(effects=[]) + @guppy(effects=None) def pure_func(x: int) -> int: return x + 1 From e53254877d24ffaaa6380ed37d40acdfe78cec8c Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 13:14:46 +0100 Subject: [PATCH 67/81] correct issue link --- tests/error/poly_errors/non_linear2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index 598f67a4c..005f85fba 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -7,8 +7,8 @@ T = guppy.type_var("T") -# Pending https://github.com/Quantinuum/guppylang/issues/1752 we need to explicitly -# declare the effects of `f` +# Pending https://github.com/Quantinuum/guppylang/issues/1760 we need to explicitly +# declare the effects of `x` @guppy.declare def foo(x: Callable[[T], T] @ effects()) -> None: ... From 458de9a10ed61bcf975549f287ece25c4515dfac Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 13:16:25 +0100 Subject: [PATCH 68/81] use set when checking invariance --- guppylang-internals/src/guppylang_internals/tys/ty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/ty.py b/guppylang-internals/src/guppylang_internals/tys/ty.py index 43a68d333..76306562e 100644 --- a/guppylang-internals/src/guppylang_internals/tys/ty.py +++ b/guppylang-internals/src/guppylang_internals/tys/ty.py @@ -910,7 +910,7 @@ def unify(s: Type | Const, t: Type | Const, subst: "Subst | None") -> "Subst | N case FunctionType() as s, FunctionType() as t if s.params == t.params: if len(s.inputs) != len(t.inputs): return None - if s.effects != t.effects: + if set(s.effects) != set(t.effects): # There are no "effect variables" yet, and we enforce exact matching # (invariance) as covariance will become difficult when we replace Order # edges with explicit tokens. (Requiring runtime closures or codegen for From dac0e57155c1aca1cc18d2331cfc292691bb342e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 13:31:35 +0100 Subject: [PATCH 69/81] Skip the explicit list of allowed effects when its in the decorator --- .../src/guppylang_internals/checker/cfg_checker.py | 4 ++-- .../src/guppylang_internals/checker/core.py | 2 +- .../guppylang_internals/checker/errors/type_errors.py | 10 +++++----- .../src/guppylang_internals/checker/expr_checker.py | 6 ++++++ .../src/guppylang_internals/checker/func_checker.py | 2 +- .../guppylang_internals/checker/modifier_checker.py | 2 +- .../src/guppylang_internals/definition/overloaded.py | 2 +- .../effects_errors/pure_calls_explicit_callable.err | 2 +- .../error/effects_errors/pure_calls_explicit_decl.err | 2 +- tests/error/effects_errors/pure_calls_explicit_def.err | 2 +- .../effects_errors/pure_calls_impure_callable.err | 2 +- tests/error/effects_errors/pure_calls_impure_decl.err | 2 +- tests/error/effects_errors/pure_calls_impure_def.err | 2 +- 13 files changed, 23 insertions(+), 17 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 18ab148c5..364def4de 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -78,7 +78,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects_from: tuple[list[Effect], Span] | None, + max_effects_from: tuple[list[Effect], Span | ast.expr] | None, first_modifier_node: ast.expr | None = None, ) -> CheckedCFG[Place]: """Instantiates a control-flow graph with the given `generic_args` and then type @@ -233,7 +233,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects_from: tuple[list[Effect], Span] | None, + max_effects_from: tuple[list[Effect], Span | ast.expr] | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index f1a596dbc..e3606f8e8 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -452,7 +452,7 @@ class Context(NamedTuple): """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: tuple[list[Effect], Span] | None = None + max_effects_from: tuple[list[Effect], Span | ast.expr] | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 4349441fd..4bb597eea 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -64,14 +64,14 @@ def effects_str(self) -> str: @dataclass(frozen=True) class MaxFromDecl(Note): - span_label: ClassVar[str] = ( - "Allowed effects `{allowed_effects_str}` declared here" - ) - allowed_effects: list[Effect] + span_label: ClassVar[str] = "Allowed effects {allowed_effects_str}declared here" + allowed_effects: list[Effect] | None @property def allowed_effects_str(self) -> str: - return Effect.format_list(self.allowed_effects) + if self.allowed_effects is None: + return "" + return "`" + Effect.format_list(self.allowed_effects) + "` " @dataclass(frozen=True) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 33c693e10..dc9fe10cf 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1318,6 +1318,12 @@ def _check_effects(func_ty: FunctionType, ctx: Context, node: AstNode) -> None: ): loc_node = node.func if isinstance(node, ast.Call) else node effects_allowed, effects_decl = ctx.max_effects_from + if isinstance(effects_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 + effects_allowed = None + # Otherwise, the error message points at all decorators, which may or may not + # list the allowed effects, so list them explicitly raise GuppyTypeError( TooManyEffectsError(loc_node, func_ty, func_ty.effects).add_sub_diagnostic( TooManyEffectsError.MaxFromDecl(effects_decl, effects_allowed) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 7c034410b..5015f21e4 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -164,7 +164,7 @@ def check_global_func_def( if ty.declared_effects is None: max_effects_from = None elif (deco := _find_guppy_decorator(func_def.decorator_list)) is not None: - max_effects_from = (ty.declared_effects, to_span(deco)) + max_effects_from = (ty.declared_effects, deco) else: # Could not identify decorator, so include all in context; union with # returns will include name etc. inbetween but avoid the function body. diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index 5dedf01dc..3cd66bbff 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -25,7 +25,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects_from: tuple[list[Effect], Span] | None, + max_effects_from: tuple[list[Effect], Span | ast.expr] | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index eb614b464..43e1e986f 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -40,7 +40,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None - max_effects_from: tuple[list[Effect], Span] | None + max_effects_from: tuple[list[Effect], Span | ast.expr] | None @property def rendered_span_label(self) -> str: diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 157c8a2df..1f0b353d8 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -10,6 +10,6 @@ Note: | 5 | 6 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err index e130c5f68..1649269de 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.err +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -10,6 +10,6 @@ Note: | 5 | 6 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err index a75aeaa1b..0a4e7f382 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.err +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -10,6 +10,6 @@ Note: | 6 | 7 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 277c2c2fc..e3fb8ff73 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -10,6 +10,6 @@ Note: | 4 | 5 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index f3751dad4..0f79bbd57 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -10,6 +10,6 @@ Note: | 5 | 6 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 4a7c268d1..b0fbe890b 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -10,6 +10,6 @@ Note: | 6 | 7 | @guppy(effects=[]) - | ----------------- Allowed effects `[]` declared here + | ----------------- Allowed effects declared here Guppy compilation failed due to 1 previous error From 33948ecc433dedb3e7bf915c23d5f1f033b5d256 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 14:34:35 +0100 Subject: [PATCH 70/81] change tuple[list[Effect], Span | expr] -> EffectLimitDecl, add s --- .../checker/cfg_checker.py | 7 ++- .../src/guppylang_internals/checker/core.py | 10 ++++- .../checker/expr_checker.py | 11 +++-- .../checker/func_checker.py | 43 +++++++++++-------- .../checker/modifier_checker.py | 6 +-- .../definition/overloaded.py | 12 +++--- tests/error/effects_errors/overload.err | 2 +- tests/error/effects_errors/pure_result.err | 2 +- 8 files changed, 53 insertions(+), 40 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py index 364def4de..00606b6b3 100644 --- a/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/cfg_checker.py @@ -15,6 +15,7 @@ from guppylang_internals.cfg.cfg import CFG, BaseCFG from guppylang_internals.checker.core import ( Context, + EffectLimitDecl, Globals, Locals, Place, @@ -25,8 +26,6 @@ from guppylang_internals.checker.stmt_checker import StmtChecker from guppylang_internals.diagnostic import Error, Note from guppylang_internals.error import GuppyError -from guppylang_internals.span import Span -from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.ty import InputFlags, Type @@ -78,7 +77,7 @@ def check_cfg( generic_args: dict[str, Argument], func_name: str, globals: Globals, - max_effects_from: tuple[list[Effect], Span | ast.expr] | None, + 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 @@ -233,7 +232,7 @@ def check_bb( return_ty: Type, generic_args: dict[str, Argument], globals: Globals, - max_effects_from: tuple[list[Effect], Span | ast.expr] | None, + max_effects_from: EffectLimitDecl | None, ) -> CheckedBB[Variable]: cfg = bb.containing_cfg diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index e3606f8e8..81c6526a5 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -443,6 +443,14 @@ 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 + + class Context(NamedTuple): """The type checking context.""" @@ -452,7 +460,7 @@ class Context(NamedTuple): """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: tuple[list[Effect], Span | ast.expr] | None = None + max_effects_from: EffectLimitDecl | None = None @property def parsing_ctx(self) -> "TypeParsingCtx": diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index dc9fe10cf..419b70b2c 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1313,12 +1313,11 @@ def check_comptime_arg( 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 ctx.max_effects_from is not None and ( - any(e not in ctx.max_effects_from[0] for e in func_ty.effects) - ): + mf = ctx.max_effects_from + if mf is not None and (any(e not in mf.effects for e in func_ty.effects)): loc_node = node.func if isinstance(node, ast.Call) else node - effects_allowed, effects_decl = ctx.max_effects_from - if isinstance(effects_decl, ast.expr): + 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 effects_allowed = None @@ -1326,7 +1325,7 @@ def _check_effects(func_ty: FunctionType, ctx: Context, node: AstNode) -> None: # list the allowed effects, so list them explicitly raise GuppyTypeError( TooManyEffectsError(loc_node, func_ty, func_ty.effects).add_sub_diagnostic( - TooManyEffectsError.MaxFromDecl(effects_decl, effects_allowed) + TooManyEffectsError.MaxFromDecl(mf.decl, effects_allowed) ) ) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 5015f21e4..98d1d9817 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -16,7 +16,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 @@ -163,23 +169,26 @@ def check_global_func_def( } if ty.declared_effects is None: max_effects_from = None - elif (deco := _find_guppy_decorator(func_def.decorator_list)) is not None: - max_effects_from = (ty.declared_effects, 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 - - max_effects_from = ( + 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)) + + max_effects_from = EffectLimitDecl( ty.declared_effects, - reduce(union, (to_span(e) for e in elems)), + decl, ) return check_cfg( cfg, @@ -216,7 +225,7 @@ def check_nested_func_def( # 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. func_ty = check_signature(func_def, ctx.globals).with_effects( - None if ctx.max_effects_from is None else ctx.max_effects_from[0] + None if ctx.max_effects_from is None else ctx.max_effects_from.effects ) assert func_ty.input_names is not None diff --git a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py index 3cd66bbff..c799e54ea 100644 --- a/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/modifier_checker.py @@ -5,13 +5,11 @@ from guppylang_internals.ast_util import loop_in_ast, 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.errors.generic import InvalidUnderDagger from guppylang_internals.definition.common import DefId from guppylang_internals.error import GuppyError from guppylang_internals.nodes import CheckedModifiedBlock, ModifiedBlock -from guppylang_internals.span import Span -from guppylang_internals.tys import Effect from guppylang_internals.tys.ty import ( FuncInput, FunctionType, @@ -25,7 +23,7 @@ def check_modified_block( modified_block: ModifiedBlock, bb: BB, ctx: Context, - max_effects_from: tuple[list[Effect], Span | ast.expr] | None, + max_effects_from: EffectLimitDecl | None, ) -> CheckedModifiedBlock: """Type checks a modifier definition.""" cfg = modified_block.cfg diff --git a/guppylang-internals/src/guppylang_internals/definition/overloaded.py b/guppylang-internals/src/guppylang_internals/definition/overloaded.py index 43e1e986f..84ae721b9 100644 --- a/guppylang-internals/src/guppylang_internals/definition/overloaded.py +++ b/guppylang-internals/src/guppylang_internals/definition/overloaded.py @@ -8,7 +8,7 @@ from typing_extensions import override from guppylang_internals.ast_util import AstNode -from guppylang_internals.checker.core import Context +from guppylang_internals.checker.core import Context, EffectLimitDecl from guppylang_internals.checker.expr_checker import ExprSynthesizer from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.definition.common import ( @@ -40,7 +40,7 @@ class OverloadNoMatchError(Error): func: str arg_tys: list[Type] return_ty: Type | None - max_effects_from: tuple[list[Effect], Span | ast.expr] | None + max_effects_from: EffectLimitDecl | None @property def rendered_span_label(self) -> str: @@ -56,9 +56,9 @@ def rendered_span_label(self) -> str: if self.return_ty: stem += f" and returns `{self.return_ty}`" if self.max_effects_from: - effects, _node = self.max_effects_from + effects = self.max_effects_from.effects if Effect.ANY not in effects: - stem += f" with effects no more than {effects}" + stem += f" with effects no more than `{effects}`" return stem @@ -120,7 +120,7 @@ def check_call( @override def synthesize_call( - self, args: list[ast.expr], node: AstNode, ctx: "Context" + self, args: list[ast.expr], node: AstNode, ctx: Context ) -> tuple[ast.expr, Type]: available_sigs: list[OverloadVariant] = [] for def_id in self.func_ids: @@ -140,7 +140,7 @@ def _call_error( self, args: list[ast.expr], node: AstNode, - ctx: "Context", + ctx: Context, available_sigs: list[OverloadVariant], return_ty: Type | None = None, ) -> NoReturn: diff --git a/tests/error/effects_errors/overload.err b/tests/error/effects_errors/overload.err index 529e4f994..9bbd80b13 100644 --- a/tests/error/effects_errors/overload.err +++ b/tests/error/effects_errors/overload.err @@ -5,7 +5,7 @@ Error: Invalid call of overloaded function (at $FILE:24:11) 24 | return only_pure_for_int(x) | ^^^^^^^^^^^^^^^^^^^^ No variant of overloaded function `only_pure_for_int` takes | a `float` argument and returns `float` with effects no more - | than [] + | than `[]` Note: Available overloads are: def only_pure_for_int(x: T) -> T diff --git a/tests/error/effects_errors/pure_result.err b/tests/error/effects_errors/pure_result.err index fdacbc842..4cc73f9ed 100644 --- a/tests/error/effects_errors/pure_result.err +++ b/tests/error/effects_errors/pure_result.err @@ -4,7 +4,7 @@ Error: Invalid call of overloaded function (at $FILE:6:10) 5 | def main() -> int: 6 | result("foo", True) | ^^^^^^^^^^^ No variant of overloaded function `result` takes arguments - | `str`, `bool` with effects no more than [] + | `str`, `bool` with effects no more than `[]` Note: Available overloads are: def result(tag: str @comptime, value: int) -> None From fa3ab4ef3abbcb16a230fe691be3ad9cb889f452 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 14:35:27 +0100 Subject: [PATCH 71/81] Add decl_name to EffectLimitDecl and MaxFromDecl --- guppylang-internals/src/guppylang_internals/checker/core.py | 1 + .../src/guppylang_internals/checker/errors/type_errors.py | 3 ++- .../src/guppylang_internals/checker/expr_checker.py | 4 +++- .../src/guppylang_internals/checker/func_checker.py | 1 + tests/error/effects_errors/pure_calls_explicit_callable.err | 4 ++-- tests/error/effects_errors/pure_calls_explicit_decl.err | 4 ++-- tests/error/effects_errors/pure_calls_explicit_def.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_callable.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_custom_def.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_decl.err | 4 ++-- tests/error/effects_errors/pure_calls_impure_def.err | 4 ++-- 11 files changed, 21 insertions(+), 16 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index 81c6526a5..f63e9c2ce 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -449,6 +449,7 @@ class EffectLimitDecl: effects: list[Effect] decl: ast.expr | Span + decl_name: str class Context(NamedTuple): diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 4bb597eea..7bcb9c402 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -53,10 +53,11 @@ class ConstMismatchError(Error): class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects {effects_str} that exceed those allowed" + "Callee of type `{ty}` has effects {effects_str} not allowed inside `{in_func}`" ) ty: Type effects: list[Effect] + in_func: str @property def effects_str(self) -> str: diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 419b70b2c..baef80bae 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1324,7 +1324,9 @@ def _check_effects(func_ty: FunctionType, ctx: Context, node: AstNode) -> None: # Otherwise, the error message points at all decorators, which may or may not # list the allowed effects, so list them explicitly raise GuppyTypeError( - TooManyEffectsError(loc_node, func_ty, func_ty.effects).add_sub_diagnostic( + TooManyEffectsError( + loc_node, func_ty, func_ty.effects, mf.decl_name + ).add_sub_diagnostic( TooManyEffectsError.MaxFromDecl(mf.decl, effects_allowed) ) ) diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 98d1d9817..5e7bc61ee 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -189,6 +189,7 @@ def union(s1: Span, s2: Span) -> Span: max_effects_from = EffectLimitDecl( ty.declared_effects, decl, + func_def.name, ) return check_cfg( cfg, diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 1f0b353d8..99d9301bb 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: 8 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that - | exceed those allowed + | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not + | allowed inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err index 1649269de..a307c3c8e 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.err +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that - | exceed those allowed + | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not + | allowed inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err index 0a4e7f382..ae30ae923 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.err +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] that - | exceed those allowed + | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not + | allowed inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index e3fb8ff73..1227e3b47 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:7:10) 5 | @guppy(effects=[]) 6 | def main(impure_f: Callable[[int], int]) -> int: 7 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed - | those allowed + | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed + | inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err index 6181804af..cf88841b2 100644 --- a/tests/error/effects_errors/pure_calls_impure_custom_def.err +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:12:10) 10 | @custom_pure 11 | def main() -> int: 12 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed - | those allowed + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed + | inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 0f79bbd57..56d70e253 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed - | those allowed + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed + | inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index b0fbe890b..495321fe8 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] that exceed - | those allowed + | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed + | inside `main` Note: | From 1f5ff09e64bb33a1f09a16d288fbfe6a41efb295 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 15:02:47 +0100 Subject: [PATCH 72/81] TooManyEffectsError: display target name with effects, or just type with effects only if hidden --- .../checker/errors/type_errors.py | 19 +++++++++++++------ .../checker/expr_checker.py | 16 ++++++++++------ .../pure_calls_explicit_callable.err | 3 +-- .../pure_calls_explicit_decl.err | 4 ++-- .../pure_calls_explicit_def.err | 4 ++-- .../pure_calls_impure_callable.err | 2 +- .../pure_calls_impure_custom_def.err | 4 ++-- .../effects_errors/pure_calls_impure_decl.err | 4 ++-- .../effects_errors/pure_calls_impure_def.err | 4 ++-- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py index 7bcb9c402..a1c12471c 100644 --- a/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py +++ b/guppylang-internals/src/guppylang_internals/checker/errors/type_errors.py @@ -52,16 +52,23 @@ class ConstMismatchError(Error): @dataclass(frozen=True) class TooManyEffectsError(Error): title: ClassVar[str] = "Too many effects" - span_label: ClassVar[str] = ( - "Callee of type `{ty}` has effects {effects_str} not allowed inside `{in_func}`" - ) - ty: Type + span_label: ClassVar[str] = "{target} not allowed inside `{in_func}`" + callee: str | FunctionType effects: list[Effect] in_func: str @property - def effects_str(self) -> str: - return Effect.format_list(self.effects) + 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): diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index baef80bae..450f045ea 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1313,21 +1313,25 @@ def check_comptime_arg( 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.""" - mf = ctx.max_effects_from - if mf is not None and (any(e not in mf.effects for e in func_ty.effects)): + 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 - effects_allowed = mf.effects + 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 - effects_allowed = None + 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 raise GuppyTypeError( TooManyEffectsError( - loc_node, func_ty, func_ty.effects, mf.decl_name + loc_node, callee, surplus_effects, mf.decl_name ).add_sub_diagnostic( - TooManyEffectsError.MaxFromDecl(mf.decl, effects_allowed) + TooManyEffectsError.MaxFromDecl(mf.decl, show_effects_allowed) ) ) diff --git a/tests/error/effects_errors/pure_calls_explicit_callable.err b/tests/error/effects_errors/pure_calls_explicit_callable.err index 99d9301bb..9ec6e30e9 100644 --- a/tests/error/effects_errors/pure_calls_explicit_callable.err +++ b/tests/error/effects_errors/pure_calls_explicit_callable.err @@ -3,8 +3,7 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: 8 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not - | allowed inside `main` + | ^^^^^^^^ Callee of type `int -[ANY]-> int` not allowed inside `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_explicit_decl.err b/tests/error/effects_errors/pure_calls_explicit_decl.err index a307c3c8e..ae32326b9 100644 --- a/tests/error/effects_errors/pure_calls_explicit_decl.err +++ b/tests/error/effects_errors/pure_calls_explicit_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not - | allowed inside `main` + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_explicit_def.err b/tests/error/effects_errors/pure_calls_explicit_def.err index ae30ae923..b24b4cced 100644 --- a/tests/error/effects_errors/pure_calls_explicit_def.err +++ b/tests/error/effects_errors/pure_calls_explicit_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -[ANY]-> int` has effects [ANY] not - | allowed inside `main` + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_callable.err b/tests/error/effects_errors/pure_calls_impure_callable.err index 1227e3b47..e1376c5d1 100644 --- a/tests/error/effects_errors/pure_calls_impure_callable.err +++ b/tests/error/effects_errors/pure_calls_impure_callable.err @@ -3,7 +3,7 @@ Error: Too many effects (at $FILE:7:10) 5 | @guppy(effects=[]) 6 | def main(impure_f: Callable[[int], int]) -> int: 7 | return impure_f(5) - | ^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed + | ^^^^^^^^ Callee of type `int -> int` has effects `[ANY]` not allowed | inside `main` Note: diff --git a/tests/error/effects_errors/pure_calls_impure_custom_def.err b/tests/error/effects_errors/pure_calls_impure_custom_def.err index cf88841b2..0771e85a4 100644 --- a/tests/error/effects_errors/pure_calls_impure_custom_def.err +++ b/tests/error/effects_errors/pure_calls_impure_custom_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:12:10) 10 | @custom_pure 11 | def main() -> int: 12 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed - | inside `main` + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_decl.err b/tests/error/effects_errors/pure_calls_impure_decl.err index 56d70e253..ae32326b9 100644 --- a/tests/error/effects_errors/pure_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_calls_impure_decl.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:8:10) 6 | @guppy(effects=[]) 7 | def main() -> int: 8 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed - | inside `main` + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` Note: | diff --git a/tests/error/effects_errors/pure_calls_impure_def.err b/tests/error/effects_errors/pure_calls_impure_def.err index 495321fe8..b24b4cced 100644 --- a/tests/error/effects_errors/pure_calls_impure_def.err +++ b/tests/error/effects_errors/pure_calls_impure_def.err @@ -3,8 +3,8 @@ Error: Too many effects (at $FILE:9:10) 7 | @guppy(effects=[]) 8 | def main() -> int: 9 | return impure_func(5) - | ^^^^^^^^^^^ Callee of type `int -> int` has effects [ANY] not allowed - | inside `main` + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` Note: | From 74d1453483acbdcf8f4333cdab3146624df860ec Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Jun 2026 15:04:28 +0100 Subject: [PATCH 73/81] drop pipeline comment --- guppylang/src/guppylang/std/qsystem/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index e6c0d23f0..19cf46f1a 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -256,7 +256,6 @@ class Measurement: """Represents the result of a lazy measurement which needs to be explicitly read before being used.""" - # We do *not* model the pipeline as a side-effect @custom_function(compiler=ReadFutureBoolCompiler(), effects=[]) @no_type_check def read(self: "Measurement" @ owned) -> bool: From 3810760ae40dea9928f2ba8be87f5dd2c5f944c9 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 7 Jun 2026 20:32:56 +0100 Subject: [PATCH 74/81] Remove external decorator enum Effect, just re-export --- .../src/guppylang_internals/decorator.py | 2 +- guppylang/src/guppylang/decorator.py | 30 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index c0c5fa9c9..8115004bd 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -115,7 +115,7 @@ def dec(f: Callable[P, T]) -> GuppyFunctionDefinition[P, T]: signature, unitary_flags, has_var_args, - effects=None if effects is None else [e.to_internal() for e in effects], + effects=effects, ) DEF_STORE.register_def(func, get_calling_frame()) return GuppyFunctionDefinition(func) diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 638516df9..2f4dddebb 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -2,7 +2,6 @@ import builtins import inspect from collections.abc import Callable, Sequence -from enum import Enum from types import FrameType from typing import ( Any, @@ -55,7 +54,7 @@ from guppylang_internals.metadata.common import FunctionMetadata from guppylang_internals.span import Loc, SourceMap, Span from guppylang_internals.tracing.util import hide_trace -from guppylang_internals.tys import Effect as _Effect +from guppylang_internals.tys import Effect # Re-exported from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst @@ -69,7 +68,7 @@ from hugr import tys as ht from hugr import val as hv from hugr.package import ModulePointer -from typing_extensions import Unpack, assert_never, dataclass_transform, deprecated +from typing_extensions import Unpack, dataclass_transform, deprecated from guppylang.defs import ( GuppyDefinition, @@ -95,18 +94,12 @@ OverloadedFunctionDef, ) -__all__ = ("Effect", "GuppyKwargs", "custom_guppy_decorator", "guppy") - - -class Effect(Enum): - ANY = "ANY" - - def to_internal(self) -> _Effect: - match self: - case Effect.ANY: - return _Effect.ANY - case _ as unreachable: - assert_never(unreachable) +__all__ = ( + "Effect", # Re-export + "GuppyKwargs", + "custom_guppy_decorator", + "guppy", +) class GuppyKwargs(TypedDict, total=False): @@ -853,7 +846,7 @@ class ParsedGuppyKwargs(NamedTuple): metadata: FunctionMetadata # The empty list means no effects, whereas None means unspecified - i.e. assume all # effects are possible until we can analyse the call-graph to calculate exactly. - effects: list[_Effect] | None + effects: list[Effect] | None link_name: str | None @@ -878,14 +871,15 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: link_name = kwargs.pop("link_name", None) + effects: list[Effect] | None if "effects" in kwargs: max_effects_input = kwargs.pop("effects") effects = ( [] if max_effects_input is None - else [max_effects_input.to_internal()] + else [max_effects_input] if isinstance(max_effects_input, Effect) - else [effect.to_internal() for effect in max_effects_input] + else max_effects_input ) else: # Not specified From 975bb02bec407cd5f98e8ed468d9d860b4aec0ff Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 7 Jun 2026 20:42:06 +0100 Subject: [PATCH 75/81] Internals decorator use internals.tys.Effect not re-export --- guppylang-internals/src/guppylang_internals/decorator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/decorator.py b/guppylang-internals/src/guppylang_internals/decorator.py index 8115004bd..f59460e6c 100644 --- a/guppylang-internals/src/guppylang_internals/decorator.py +++ b/guppylang-internals/src/guppylang_internals/decorator.py @@ -59,8 +59,7 @@ from collections.abc import Callable, Sequence from types import FrameType - from guppylang import Effect - + from guppylang_internals.tys import Effect from guppylang_internals.tys.arg import Argument from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst From 4e64ea84823dc75ae99fc005f964d5bfca6304b1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 6 Jun 2026 09:44:27 +0100 Subject: [PATCH 76/81] Tests of comptime (some failing) --- tests/integration/test_comptime_effects.py | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/integration/test_comptime_effects.py diff --git a/tests/integration/test_comptime_effects.py b/tests/integration/test_comptime_effects.py new file mode 100644 index 000000000..0ac1feb93 --- /dev/null +++ b/tests/integration/test_comptime_effects.py @@ -0,0 +1,167 @@ +"""Tests of effects annotation for comptime callers/callees.""" + +import pytest + +from guppylang.decorator import Effect, guppy +from guppylang.std.builtins import result +from guppylang.std.effects import ANY + + +def test_pure_from_impure_comptime(validate): + @guppy(effects=None) + def pure_func(x: int) -> int: + return x + 1 + + @guppy.comptime + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +def test_pure_comptime_from_impure(validate): + @guppy.comptime(effects=None) + def pure_func(x: int) -> int: + return x + 1 + + @guppy + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +def test_comptime_pure_from_impure(validate): + @guppy.comptime(effects=[]) + def pure_func(x: int) -> int: + return x + 1 + + @guppy.comptime + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +def test_pure_from_explicit_comptime_impure(validate): + @guppy(effects=[]) + def pure_func(x: int) -> int: + return x + 1 + + @guppy.comptime(effects=ANY) + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +def test_pure_comptime_from_explicit_impure(validate): + @guppy.comptime(effects=None) + def pure_func(x: int) -> int: + return x + 1 + + @guppy(effects=[Effect.ANY]) + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +def test_comptime_pure_from_explicit_impure(validate): + @guppy.comptime(effects=[]) + def pure_func(x: int) -> int: + return x + 1 + + @guppy.comptime + def normal_func(x: int) -> int: + return pure_func(x) + 2 + + validate(normal_func.compile_function()) + + +@pytest.mark.parametrize( + ("caller_flags", "callee"), + [ + ({"effects": [Effect.ANY]}, {}), + ({}, {"effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {"effects": [Effect.ANY]}), + ], +) +def test_impure_explicit_from_comptime(caller_flags, callee, validate): + @guppy(**callee) + def impure_func(x: int) -> int: + result("tag", x) + return x + 3 + + @guppy.comptime(**caller_flags) + def caller(x: int) -> int: + return impure_func(x) + 1 + + validate(caller.compile_function()) + + +@pytest.mark.parametrize( + ("caller", "callee"), + [ + ({"effects": [Effect.ANY]}, {}), + ({}, {"effects": [Effect.ANY]}), + ({"effects": [Effect.ANY]}, {"effects": [Effect.ANY]}), + ], +) +@pytest.mark.parametrize( + ("caller_deco", "callee_deco"), + [ + (guppy.comptime, guppy), + (guppy, guppy.comptime), + (guppy.comptime, guppy.comptime), + ], +) +def test_impure_explicit_comptime_callee( + caller, callee, caller_deco, callee_deco, validate +): + @callee_deco(**callee) + def impure_func(x: int) -> int: + result("tag", x) + return x + 3 + + @caller_deco(**caller) + def impure_func2(x: int) -> int: + return impure_func(x) + 1 + + validate(impure_func2.compile_function()) + + +def test_pure_from_pure_comptime(validate): + @guppy(effects=[]) + def pure_func1(x: int) -> int: + return x + 1 + + @guppy.comptime(effects=None) + def pure_func2(x: int) -> int: + return pure_func1(pure_func1(x)) + 1 + + validate(pure_func2.compile_function()) + + +def test_pure_comptime_from_pure(validate): + @guppy.comptime(effects=None) + def pure_func1(x: int) -> int: + return x + 1 + + @guppy(effects=[]) + def pure_func2(x: int) -> int: + return pure_func1(pure_func1(x)) + 1 + + validate(pure_func2.compile_function()) + + +def test_comptime_pure_from_pure(validate): + @guppy.comptime(effects=[]) + def pure_func1(x: int) -> int: + return x + 1 + + @guppy.comptime(effects=[]) + def pure_func2(x: int) -> int: + return pure_func1(pure_func1(x)) + 1 + + validate(pure_func2.compile_function()) From d5b45afff7344ae6aba43a0a4b85ace4cf4fdfe7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 6 Jun 2026 09:48:22 +0100 Subject: [PATCH 77/81] Add comptime error tests (messages wrong atm) --- .../effects_errors/comptime_pure_calls_impure.err | 15 +++++++++++++++ .../effects_errors/comptime_pure_calls_impure.py | 11 +++++++++++ .../pure_calls_explicit_callable_comptime.err | 14 ++++++++++++++ .../pure_calls_explicit_callable_comptime.py | 10 ++++++++++ .../pure_calls_impure_callable_comptime.err | 15 +++++++++++++++ .../pure_calls_impure_callable_comptime.py | 9 +++++++++ .../effects_errors/pure_calls_impure_comptime.err | 15 +++++++++++++++ .../effects_errors/pure_calls_impure_comptime.py | 11 +++++++++++ .../pure_comptime_calls_impure_decl.err | 15 +++++++++++++++ .../pure_comptime_calls_impure_decl.py | 11 +++++++++++ .../pure_comptime_calls_impure_def.err | 15 +++++++++++++++ .../pure_comptime_calls_impure_def.py | 11 +++++++++++ .../return_explicit_callable_comptime.err | 9 +++++++++ .../return_explicit_callable_comptime.py | 14 ++++++++++++++ .../return_impure_callable_comptime.err | 9 +++++++++ .../return_impure_callable_comptime.py | 14 ++++++++++++++ .../return_pure_callable_comptime.err | 9 +++++++++ .../return_pure_callable_comptime.py | 14 ++++++++++++++ 18 files changed, 221 insertions(+) create mode 100644 tests/error/effects_errors/comptime_pure_calls_impure.err create mode 100644 tests/error/effects_errors/comptime_pure_calls_impure.py create mode 100644 tests/error/effects_errors/pure_calls_explicit_callable_comptime.err create mode 100644 tests/error/effects_errors/pure_calls_explicit_callable_comptime.py create mode 100644 tests/error/effects_errors/pure_calls_impure_callable_comptime.err create mode 100644 tests/error/effects_errors/pure_calls_impure_callable_comptime.py create mode 100644 tests/error/effects_errors/pure_calls_impure_comptime.err create mode 100644 tests/error/effects_errors/pure_calls_impure_comptime.py create mode 100644 tests/error/effects_errors/pure_comptime_calls_impure_decl.err create mode 100644 tests/error/effects_errors/pure_comptime_calls_impure_decl.py create mode 100644 tests/error/effects_errors/pure_comptime_calls_impure_def.err create mode 100644 tests/error/effects_errors/pure_comptime_calls_impure_def.py create mode 100644 tests/error/effects_errors/return_explicit_callable_comptime.err create mode 100644 tests/error/effects_errors/return_explicit_callable_comptime.py create mode 100644 tests/error/effects_errors/return_impure_callable_comptime.err create mode 100644 tests/error/effects_errors/return_impure_callable_comptime.py create mode 100644 tests/error/effects_errors/return_pure_callable_comptime.err create mode 100644 tests/error/effects_errors/return_pure_callable_comptime.py diff --git a/tests/error/effects_errors/comptime_pure_calls_impure.err b/tests/error/effects_errors/comptime_pure_calls_impure.err new file mode 100644 index 000000000..b24b4cced --- /dev/null +++ b/tests/error/effects_errors/comptime_pure_calls_impure.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` + +Note: + | +6 | +7 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/comptime_pure_calls_impure.py b/tests/error/effects_errors/comptime_pure_calls_impure.py new file mode 100644 index 000000000..f17ebd731 --- /dev/null +++ b/tests/error/effects_errors/comptime_pure_calls_impure.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy + +@guppy.comptime +def impure_func(x: int) -> int: + return x + 1 + +@guppy.comptime(effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_calls_explicit_callable_comptime.err b/tests/error/effects_errors/pure_calls_explicit_callable_comptime.err new file mode 100644 index 000000000..9ec6e30e9 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_callable_comptime.err @@ -0,0 +1,14 @@ +Error: Too many effects (at $FILE:8:10) + | +6 | @guppy(effects=[]) +7 | def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: +8 | return impure_f(5) + | ^^^^^^^^ Callee of type `int -[ANY]-> int` not allowed inside `main` + +Note: + | +5 | +6 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_explicit_callable_comptime.py b/tests/error/effects_errors/pure_calls_explicit_callable_comptime.py new file mode 100644 index 000000000..d3a5a60d1 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_explicit_callable_comptime.py @@ -0,0 +1,10 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy +from guppylang.std.effects import effects, ANY + +@guppy(effects=[]) +def main(impure_f: Callable[[int], int] @ effects(ANY)) -> int: + return impure_f(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_calls_impure_callable_comptime.err b/tests/error/effects_errors/pure_calls_impure_callable_comptime.err new file mode 100644 index 000000000..e1376c5d1 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_callable_comptime.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:7:10) + | +5 | @guppy(effects=[]) +6 | def main(impure_f: Callable[[int], int]) -> int: +7 | return impure_f(5) + | ^^^^^^^^ Callee of type `int -> int` has effects `[ANY]` not allowed + | inside `main` + +Note: + | +4 | +5 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_callable_comptime.py b/tests/error/effects_errors/pure_calls_impure_callable_comptime.py new file mode 100644 index 000000000..eb8e4cb97 --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_callable_comptime.py @@ -0,0 +1,9 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy + +@guppy(effects=[]) +def main(impure_f: Callable[[int], int]) -> int: + return impure_f(5) + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/pure_calls_impure_comptime.err b/tests/error/effects_errors/pure_calls_impure_comptime.err new file mode 100644 index 000000000..b24b4cced --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_comptime.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` + +Note: + | +6 | +7 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_calls_impure_comptime.py b/tests/error/effects_errors/pure_calls_impure_comptime.py new file mode 100644 index 000000000..46f7b402f --- /dev/null +++ b/tests/error/effects_errors/pure_calls_impure_comptime.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy + +@guppy.comptime +def impure_func(x: int) -> int: + return x + 1 + +@guppy(effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_decl.err b/tests/error/effects_errors/pure_comptime_calls_impure_decl.err new file mode 100644 index 000000000..b24b4cced --- /dev/null +++ b/tests/error/effects_errors/pure_comptime_calls_impure_decl.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` + +Note: + | +6 | +7 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_decl.py b/tests/error/effects_errors/pure_comptime_calls_impure_decl.py new file mode 100644 index 000000000..6c2341ae2 --- /dev/null +++ b/tests/error/effects_errors/pure_comptime_calls_impure_decl.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy + +@guppy.declare +def impure_func(x: int) -> int: + ... + +@guppy.comptime(effects=[]) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_def.err b/tests/error/effects_errors/pure_comptime_calls_impure_def.err new file mode 100644 index 000000000..b24b4cced --- /dev/null +++ b/tests/error/effects_errors/pure_comptime_calls_impure_def.err @@ -0,0 +1,15 @@ +Error: Too many effects (at $FILE:9:10) + | +7 | @guppy(effects=[]) +8 | def main() -> int: +9 | return impure_func(5) + | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside + | `main` + +Note: + | +6 | +7 | @guppy(effects=[]) + | ----------------- Allowed effects declared here + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_def.py b/tests/error/effects_errors/pure_comptime_calls_impure_def.py new file mode 100644 index 000000000..87fa05951 --- /dev/null +++ b/tests/error/effects_errors/pure_comptime_calls_impure_def.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy + +@guppy +def impure_func(x: int) -> int: + return x + 1 + +@guppy.comptime(effects=None) +def main() -> int: + return impure_func(5) + +main.compile() diff --git a/tests/error/effects_errors/return_explicit_callable_comptime.err b/tests/error/effects_errors/return_explicit_callable_comptime.err new file mode 100644 index 000000000..4d5d950d1 --- /dev/null +++ b/tests/error/effects_errors/return_explicit_callable_comptime.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:12:10) + | +10 | @guppy +11 | def main() -> Callable[[int], int] @effects(): +12 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int + | -[ANY]-> int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_explicit_callable_comptime.py b/tests/error/effects_errors/return_explicit_callable_comptime.py new file mode 100644 index 000000000..f050f09d3 --- /dev/null +++ b/tests/error/effects_errors/return_explicit_callable_comptime.py @@ -0,0 +1,14 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy, Effect +from guppylang.std.effects import effects + +@guppy(effects=[Effect.ANY]) +def impure_func(x: int) -> int: + return x + 1 + +@guppy.comptime +def main() -> Callable[[int], int] @effects(): + return impure_func + +main.compile() diff --git a/tests/error/effects_errors/return_impure_callable_comptime.err b/tests/error/effects_errors/return_impure_callable_comptime.err new file mode 100644 index 000000000..05d5b87ad --- /dev/null +++ b/tests/error/effects_errors/return_impure_callable_comptime.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:12:10) + | +10 | @guppy +11 | def main() -> Callable[[int], int] @ effects(): +12 | return impure_func + | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> + | int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_impure_callable_comptime.py b/tests/error/effects_errors/return_impure_callable_comptime.py new file mode 100644 index 000000000..25570a8c7 --- /dev/null +++ b/tests/error/effects_errors/return_impure_callable_comptime.py @@ -0,0 +1,14 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy +from guppylang.std.effects import effects + +@guppy +def impure_func(x: int) -> int: + return x + 1 + +@guppy.comptime +def main() -> Callable[[int], int] @ effects(): + return impure_func + +main.compile() \ No newline at end of file diff --git a/tests/error/effects_errors/return_pure_callable_comptime.err b/tests/error/effects_errors/return_pure_callable_comptime.err new file mode 100644 index 000000000..e81b94dbe --- /dev/null +++ b/tests/error/effects_errors/return_pure_callable_comptime.err @@ -0,0 +1,9 @@ +Error: Type mismatch (at $FILE:12:10) + | +10 | @guppy +11 | def main() -> Callable[[int], int]: +12 | return pure_func + | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -[]-> + | int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_pure_callable_comptime.py b/tests/error/effects_errors/return_pure_callable_comptime.py new file mode 100644 index 000000000..525a652aa --- /dev/null +++ b/tests/error/effects_errors/return_pure_callable_comptime.py @@ -0,0 +1,14 @@ +from collections.abc import Callable + +from guppylang.decorator import guppy + +@guppy(effects=[]) +def pure_func(x: int) -> int: + return x + 1 + +# This is an error because we enforce invariance of Callable types. +@guppy.comptime +def main() -> Callable[[int], int]: + return pure_func + +main.compile() \ No newline at end of file From 281501f113a79718cb3283c033e73e5906f997c3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 8 Jun 2026 10:00:46 +0100 Subject: [PATCH 78/81] Refactor EffectLimitDecl.for_def --- .../src/guppylang_internals/checker/core.py | 46 ++++++++++++++++++- .../checker/func_checker.py | 40 +--------------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/core.py b/guppylang-internals/src/guppylang_internals/checker/core.py index f63e9c2ce..c6d71738b 100644 --- a/guppylang-internals/src/guppylang_internals/checker/core.py +++ b/guppylang-internals/src/guppylang_internals/checker/core.py @@ -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, @@ -26,11 +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 +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, @@ -451,6 +452,47 @@ class EffectLimitDecl: 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.""" diff --git a/guppylang-internals/src/guppylang_internals/checker/func_checker.py b/guppylang-internals/src/guppylang_internals/checker/func_checker.py index 5e7bc61ee..1937d06c9 100644 --- a/guppylang-internals/src/guppylang_internals/checker/func_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/func_checker.py @@ -9,7 +9,6 @@ import copy import sys from dataclasses import dataclass, replace -from functools import reduce from typing import TYPE_CHECKING, ClassVar, cast from guppylang_internals.ast_util import AstNode, return_nodes_in_ast, with_loc @@ -32,7 +31,6 @@ from guppylang_internals.error import GuppyError from guppylang_internals.experimental import check_capturing_closures_enabled from guppylang_internals.nodes import CheckedNestedFunctionDef, NestedFunctionDef -from guppylang_internals.span import Span, to_span from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.parsing import ( TypeParsingCtx, @@ -167,30 +165,7 @@ def check_global_func_def( generic_args = { param.name: arg for param, arg in zip(generic_ty.params, type_args, strict=True) } - if ty.declared_effects is None: - max_effects_from = None - else: - 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)) - - max_effects_from = EffectLimitDecl( - ty.declared_effects, - decl, - func_def.name, - ) + max_effects_from = EffectLimitDecl.for_def(ty, func_def) return check_cfg( cfg, inputs, @@ -202,19 +177,6 @@ def union(s1: Span, s2: Span) -> Span: ) -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 - - def check_nested_func_def( func_def: NestedFunctionDef, bb: BB, From d7665a2ac98c23e7c2c703454186b792d4069a18 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 8 Jun 2026 10:17:24 +0100 Subject: [PATCH 79/81] Implement for tracing, update error messages --- .../guppylang_internals/definition/traced.py | 2 +- .../guppylang_internals/tracing/function.py | 14 ++++++++++--- .../src/guppylang_internals/tracing/state.py | 4 ++++ .../comptime_pure_calls_impure.err | 21 ++++++------------- .../pure_comptime_calls_impure_decl.err | 21 ++++++------------- .../pure_comptime_calls_impure_def.err | 21 ++++++------------- .../return_explicit_callable_comptime.err | 8 ++++--- .../return_impure_callable_comptime.err | 8 ++++--- .../return_pure_callable_comptime.err | 8 ++++--- 9 files changed, 49 insertions(+), 58 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 8c5070474..73d9deeec 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -64,7 +64,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "TracedFunctionDef": func_ast, _docstring = parse_py_func(self.python_func, sources) ty = check_signature( func_ast, globals, self.id, unitary_flags=self.unitary_flags - ) + ).with_effects(self.effects) if ty.parametrized: raise GuppyError(UnsupportedError(func_ast, "Generic comptime functions")) return TracedFunctionDef( diff --git a/guppylang-internals/src/guppylang_internals/tracing/function.py b/guppylang-internals/src/guppylang_internals/tracing/function.py index 4677a0623..1a1d58736 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/function.py +++ b/guppylang-internals/src/guppylang_internals/tracing/function.py @@ -10,6 +10,7 @@ from guppylang_internals.checker.core import ( ComptimeVariable, Context, + EffectLimitDecl, Globals, Locals, Variable, @@ -73,7 +74,10 @@ def trace_function( Invokes the passed Python callable and constructs the corresponding Hugr using the passed builder. """ - state = TracingState(ctx, DFContainer(builder, ctx, {}), node, func_def) + max_effects = EffectLimitDecl.for_def(ty, func_def.defined_at) + state = TracingState( + ctx, DFContainer(builder, ctx, {}), node, func_def, max_effects=max_effects + ) with set_tracing_state(state): inputs = [ unpack_guppy_object( @@ -182,8 +186,12 @@ def trace_call(func: CallableDef, *args: Any) -> Any: arg_exprs: list[ast.expr] = [ with_loc(state.node, with_type(var.ty, PlaceNode(var))) for var in arg_vars ] - # ALAN add effects to Tracing? - ctx = Context(Globals(DEF_STORE.frames[func.id]), locals, {}) + ctx = Context( + Globals(DEF_STORE.frames[func.id]), + locals, + {}, + max_effects_from=state.max_effects, + ) call_node, ret_ty = func.synthesize_call(arg_exprs, state.node, ctx) # Here we check if unitary constraints are respected by the caller diff --git a/guppylang-internals/src/guppylang_internals/tracing/state.py b/guppylang-internals/src/guppylang_internals/tracing/state.py index e1969bc5c..8d11905c2 100644 --- a/guppylang-internals/src/guppylang_internals/tracing/state.py +++ b/guppylang-internals/src/guppylang_internals/tracing/state.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from guppylang_internals.ast_util import AstNode +from guppylang_internals.checker.core import EffectLimitDecl from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.definition.traced import CompiledTracedFunctionDef from guppylang_internals.error import InternalGuppyError @@ -29,6 +30,9 @@ class TracingState: #: The function definition currently being traced. function_definition: CompiledTracedFunctionDef + #: The maximum effects that the currently traced function is allowed to perform + max_effects: EffectLimitDecl | None + #: Set of all allocated undroppable GuppyObjects where the `used` flag is not set, #: indexed by their id. This is used to detect linearity violations. unused_undroppable_objs: "dict[GuppyObjectId, GuppyObject]" = field( diff --git a/tests/error/effects_errors/comptime_pure_calls_impure.err b/tests/error/effects_errors/comptime_pure_calls_impure.err index b24b4cced..32b6f4266 100644 --- a/tests/error/effects_errors/comptime_pure_calls_impure.err +++ b/tests/error/effects_errors/comptime_pure_calls_impure.err @@ -1,15 +1,6 @@ -Error: Too many effects (at $FILE:9:10) - | -7 | @guppy(effects=[]) -8 | def main() -> int: -9 | return impure_func(5) - | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside - | `main` - -Note: - | -6 | -7 | @guppy(effects=[]) - | ----------------- Allowed effects declared here - -Guppy compilation failed due to 1 previous error +Traceback (most recent call last): + File "$FILE", line 11, in + main.compile() + File "$FILE", line 9, in main + return impure_func(5) +guppylang_internals.error.GuppyComptimeError: Too many effects: Callee of type `int -> int` has effects `[ANY]` not allowed inside `main` diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_decl.err b/tests/error/effects_errors/pure_comptime_calls_impure_decl.err index b24b4cced..32b6f4266 100644 --- a/tests/error/effects_errors/pure_comptime_calls_impure_decl.err +++ b/tests/error/effects_errors/pure_comptime_calls_impure_decl.err @@ -1,15 +1,6 @@ -Error: Too many effects (at $FILE:9:10) - | -7 | @guppy(effects=[]) -8 | def main() -> int: -9 | return impure_func(5) - | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside - | `main` - -Note: - | -6 | -7 | @guppy(effects=[]) - | ----------------- Allowed effects declared here - -Guppy compilation failed due to 1 previous error +Traceback (most recent call last): + File "$FILE", line 11, in + main.compile() + File "$FILE", line 9, in main + return impure_func(5) +guppylang_internals.error.GuppyComptimeError: Too many effects: Callee of type `int -> int` has effects `[ANY]` not allowed inside `main` diff --git a/tests/error/effects_errors/pure_comptime_calls_impure_def.err b/tests/error/effects_errors/pure_comptime_calls_impure_def.err index b24b4cced..32b6f4266 100644 --- a/tests/error/effects_errors/pure_comptime_calls_impure_def.err +++ b/tests/error/effects_errors/pure_comptime_calls_impure_def.err @@ -1,15 +1,6 @@ -Error: Too many effects (at $FILE:9:10) - | -7 | @guppy(effects=[]) -8 | def main() -> int: -9 | return impure_func(5) - | ^^^^^^^^^^^ Call to `impure_func` has effects `[ANY]` not allowed inside - | `main` - -Note: - | -6 | -7 | @guppy(effects=[]) - | ----------------- Allowed effects declared here - -Guppy compilation failed due to 1 previous error +Traceback (most recent call last): + File "$FILE", line 11, in + main.compile() + File "$FILE", line 9, in main + return impure_func(5) +guppylang_internals.error.GuppyComptimeError: Too many effects: Callee of type `int -> int` has effects `[ANY]` not allowed inside `main` diff --git a/tests/error/effects_errors/return_explicit_callable_comptime.err b/tests/error/effects_errors/return_explicit_callable_comptime.err index 4d5d950d1..f3373a7c4 100644 --- a/tests/error/effects_errors/return_explicit_callable_comptime.err +++ b/tests/error/effects_errors/return_explicit_callable_comptime.err @@ -1,9 +1,11 @@ -Error: Type mismatch (at $FILE:12:10) +Error: Type mismatch (at $FILE:11:0) | -10 | @guppy + 9 | +10 | @guppy.comptime 11 | def main() -> Callable[[int], int] @effects(): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | return impure_func - | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int + | ^^^^^^^^^^^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int | -[ANY]-> int` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_impure_callable_comptime.err b/tests/error/effects_errors/return_impure_callable_comptime.err index 05d5b87ad..cf9240fe0 100644 --- a/tests/error/effects_errors/return_impure_callable_comptime.err +++ b/tests/error/effects_errors/return_impure_callable_comptime.err @@ -1,9 +1,11 @@ -Error: Type mismatch (at $FILE:12:10) +Error: Type mismatch (at $FILE:11:0) | -10 | @guppy + 9 | +10 | @guppy.comptime 11 | def main() -> Callable[[int], int] @ effects(): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | return impure_func - | ^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> + | ^^^^^^^^^^^^^^^^^^^^^ Expected return value of type `int -[]-> int`, got `int -> | int` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/return_pure_callable_comptime.err b/tests/error/effects_errors/return_pure_callable_comptime.err index e81b94dbe..3aabc3944 100644 --- a/tests/error/effects_errors/return_pure_callable_comptime.err +++ b/tests/error/effects_errors/return_pure_callable_comptime.err @@ -1,9 +1,11 @@ -Error: Type mismatch (at $FILE:12:10) +Error: Type mismatch (at $FILE:11:0) | -10 | @guppy + 9 | # This is an error because we enforce invariance of Callable types. +10 | @guppy.comptime 11 | def main() -> Callable[[int], int]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | return pure_func - | ^^^^^^^^^ Expected return value of type `int -> int`, got `int -[]-> + | ^^^^^^^^^^^^^^^^^^^ Expected return value of type `int -> int`, got `int -[]-> | int` Guppy compilation failed due to 1 previous error From 8ef3a39c3e878952fe3a0cc8c8c42b3a0bf03cfe Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 11 Jun 2026 12:36:12 +0100 Subject: [PATCH 80/81] Add backquotes to some error messages --- guppylang-internals/src/guppylang_internals/tys/errors.py | 6 +++--- tests/error/effects_errors/effects_on_int.err | 2 +- tests/error/effects_errors/misnamed_effects.err | 2 +- tests/error/effects_errors/repeated_effects.err | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/errors.py b/guppylang-internals/src/guppylang_internals/tys/errors.py index c6cac17c3..1b20392e0 100644 --- a/guppylang-internals/src/guppylang_internals/tys/errors.py +++ b/guppylang-internals/src/guppylang_internals/tys/errors.py @@ -175,21 +175,21 @@ class ComptimeArgShadowError(Error): @dataclass(frozen=True) class EffectsNotApplicableError(Error): title: ClassVar[str] = "Invalid annotation" - span_label: ClassVar[str] = "Effects may be applied only to a Callable type" + span_label: ClassVar[str] = "Effects may be applied only to a `Callable` type" @dataclass(frozen=True) class EffectsRepeatedError(Error): title: ClassVar[str] = "Invalid annotation" span_label: ClassVar[str] = ( - "Effects have already been applied to this Callable type" + "Effects have already been applied to this `Callable` type" ) @dataclass(frozen=True) class InvalidEffectError(Error): title: ClassVar[str] = "Invalid annotation" - span_label: ClassVar[str] = "Not a valid effect: {arg}" + span_label: ClassVar[str] = "Not a valid effect: `{arg}`" # We could perhaps provide a list of possible effects? arg: str diff --git a/tests/error/effects_errors/effects_on_int.err b/tests/error/effects_errors/effects_on_int.err index 34789fb1b..691f02342 100644 --- a/tests/error/effects_errors/effects_on_int.err +++ b/tests/error/effects_errors/effects_on_int.err @@ -3,6 +3,6 @@ Error: Invalid annotation (at $FILE:8:26) 6 | # This says the return type (not the function) has effects 7 | @guppy 8 | def main(x: int) -> int @ effects(): - | ^^^^^^^^^ Effects may be applied only to a Callable type + | ^^^^^^^^^ Effects may be applied only to a `Callable` type Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/misnamed_effects.err b/tests/error/effects_errors/misnamed_effects.err index d2c20c740..23ea0ea21 100644 --- a/tests/error/effects_errors/misnamed_effects.err +++ b/tests/error/effects_errors/misnamed_effects.err @@ -3,6 +3,6 @@ Error: Invalid annotation (at $FILE:7:36) 5 | 6 | @guppy.declare 7 | def foo() -> Callable[[int], int] @ effects(ALL): - | ^^^^^^^^^^^^ Not a valid effect: ALL + | ^^^^^^^^^^^^ Not a valid effect: `ALL` Guppy compilation failed due to 1 previous error diff --git a/tests/error/effects_errors/repeated_effects.err b/tests/error/effects_errors/repeated_effects.err index 7847a7aac..7f402150b 100644 --- a/tests/error/effects_errors/repeated_effects.err +++ b/tests/error/effects_errors/repeated_effects.err @@ -3,6 +3,6 @@ Error: Invalid annotation (at $FILE:7:48) 5 | 6 | @guppy.declare 7 | def foo() -> Callable[[int], int] @ effects() @ effects(ANY): - | ^^^^^^^^^^^^ Effects have already been applied to this Callable type + | ^^^^^^^^^^^^ Effects have already been applied to this `Callable` type Guppy compilation failed due to 1 previous error From 1ffbeb199773b8bcf9cd2aa357bb9777f2b0c249 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 May 2026 20:54:59 +0100 Subject: [PATCH 81/81] undo purification of enum+struct ctors, bool conversion, stdlib except int.add --- .../checker/expr_checker.py | 7 +- .../guppylang_internals/definition/enum.py | 1 - .../guppylang_internals/definition/struct.py | 1 - guppylang/src/guppylang/std/angles.py | 18 +- guppylang/src/guppylang/std/num.py | 245 ++++++++---------- .../src/guppylang/std/qsystem/__init__.py | 4 +- .../src/guppylang/std/quantum/__init__.py | 50 ++-- .../src/guppylang/std/quantum/functional.py | 42 +-- tests/error/modifier_errors/higher_order.err | 8 +- tests/error/modifier_errors/higher_order.py | 3 +- tests/error/poly_errors/non_linear2.py | 2 +- .../struct_errors/constructor_missing_arg.err | 2 +- .../constructor_too_many_args.err | 2 +- tests/error/type_errors/and_not_bool_left.err | 2 +- .../error/type_errors/and_not_bool_right.err | 2 +- tests/error/type_errors/if_expr_not_bool.err | 2 +- tests/error/type_errors/if_not_bool.err | 2 +- tests/error/type_errors/not_not_bool.err | 2 +- tests/error/type_errors/or_not_bool_left.err | 2 +- tests/error/type_errors/or_not_bool_right.err | 2 +- tests/error/type_errors/while_not_bool.err | 2 +- .../notebooks/misc_notebook_tests.ipynb | 4 +- tests/integration/test_enum.py | 3 +- tests/integration/test_struct.py | 8 +- tests/integration/tracing/test_enum.py | 3 +- tests/integration/tracing/test_struct.py | 3 +- 26 files changed, 190 insertions(+), 232 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py index 450f045ea..62be8bfb4 100644 --- a/guppylang-internals/src/guppylang_internals/checker/expr_checker.py +++ b/guppylang-internals/src/guppylang_internals/checker/expr_checker.py @@ -1528,9 +1528,6 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type return node, node_ty synth = ExprSynthesizer(ctx) exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Inout)], bool_type()) - # 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([]) try: return synth.synthesize_instance_func( node, [], "__bool__", "truthy", exp_sig, True @@ -1539,9 +1536,7 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type if not node_ty.copyable: # Linear types may implement a `__consume_as_bool__` method that consumes # the value, instead of borrowing it. - exp_sig = FunctionType( - [FuncInput(node_ty, InputFlags.Owned)], bool_type() - ).with_effects([]) + exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Owned)], bool_type()) return synth.synthesize_instance_func( node, [], "__consume_as_bool__", "truthy", exp_sig, True ) diff --git a/guppylang-internals/src/guppylang_internals/definition/enum.py b/guppylang-internals/src/guppylang_internals/definition/enum.py index bcfc20561..1bc17cf8b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/enum.py +++ b/guppylang-internals/src/guppylang_internals/definition/enum.py @@ -306,7 +306,6 @@ def compile(self, wires: list[Wire]) -> list[Wire]: ], output=enum_type, params=self.params, - declared_effects=[], ) constructor_def = CustomFunctionDef( diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index ff40842c9..ae753cb3b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -225,7 +225,6 @@ def compile(self, args: list[Wire]) -> list[Wire]: defn=self, args=[p.to_bound(i) for i, p in enumerate(self.params)] ), params=self.params, - declared_effects=[], ) constructor_def = CustomFunctionDef( id=DefId.fresh(), diff --git a/guppylang/src/guppylang/std/angles.py b/guppylang/src/guppylang/std/angles.py index 4b4fb8f5c..d44aea0b0 100644 --- a/guppylang/src/guppylang/std/angles.py +++ b/guppylang/src/guppylang/std/angles.py @@ -31,47 +31,47 @@ class angle: halfturns: float - @guppy(effects=[]) + @guppy @no_type_check def __add__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns + other.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __sub__(self: "angle", other: "angle") -> "angle": return angle(self.halfturns - other.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __mul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(effects=[]) + @guppy @no_type_check def __rmul__(self: "angle", other: float) -> "angle": return angle(self.halfturns * other) - @guppy(effects=[]) + @guppy @no_type_check def __truediv__(self: "angle", other: float) -> "angle": return angle(self.halfturns / other) - @guppy(effects=[]) + @guppy @no_type_check def __rtruediv__(self: "angle", other: float) -> "angle": return angle(other / self.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __neg__(self: "angle") -> "angle": return angle(-self.halfturns) - @guppy(effects=[]) + @guppy @no_type_check def __float__(self: "angle") -> float: return self.halfturns * py(math.pi) - @guppy(effects=[]) + @guppy @no_type_check def __eq__(self: "angle", other: "angle") -> bool: return self.halfturns == other.halfturns diff --git a/guppylang/src/guppylang/std/num.py b/guppylang/src/guppylang/std/num.py index 7b7ebc263..176f6b211 100644 --- a/guppylang/src/guppylang/std/num.py +++ b/guppylang/src/guppylang/std/num.py @@ -20,99 +20,94 @@ ) from guppylang_internals.tys.builtin import float_type_def, int_type_def, nat_type_def -from guppylang import Effect, guppy +from guppylang import guppy @extend_type(nat_type_def) class nat: """A 64-bit unsigned integer.""" - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __abs__(self: nat) -> nat: ... - @hugr_op(int_op("iadd"), effects=[]) + @hugr_op(int_op("iadd")) def __add__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("iand"), effects=[]) + @hugr_op(int_op("iand")) def __and__(self: nat, other: nat) -> nat: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: nat) -> bool: return self != 0 - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __ceil__(self: nat) -> nat: ... - # Panics if other == 0 - @hugr_op(int_op("idivmod_u", n_vars=2), effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_u", n_vars=2)) def __divmod__(self: nat, other: nat) -> tuple[nat, nat]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq"))) def __eq__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) + @hugr_op(int_op("convert_u", hugr.std.int.CONVERSIONS_EXTENSION)) def __float__(self: nat) -> float: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __floor__(self: nat) -> nat: ... - # Panics if other == 0 - @hugr_op(int_op("idiv_u"), effects=[Effect.ANY]) + @hugr_op(int_op("idiv_u")) def __floordiv__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ige_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_u"))) def __ge__(self: nat, other: nat) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_u"))) def __gt__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("iu_to_s"), effects=[]) + @hugr_op(int_op("iu_to_s")) def __int__(self: nat) -> int: ... - @hugr_op(int_op("inot"), effects=[]) + @hugr_op(int_op("inot")) def __invert__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ile_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_u"))) def __le__(self: nat, other: nat) -> bool: ... - @hugr_op(int_op("ishl"), effects=[]) + @hugr_op(int_op("ishl")) def __lshift__(self: nat, other: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ilt_u")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ilt_u"))) def __lt__(self: nat, other: nat) -> bool: ... - # Panics if other == 0 - @hugr_op(int_op("imod_u"), effects=[Effect.ANY]) + @hugr_op(int_op("imod_u")) def __mod__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("imul"), effects=[]) + @hugr_op(int_op("imul")) def __mul__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __nat__(self: nat) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ine"))) def __ne__(self: nat, other: nat) -> bool: ... - @custom_function( - checker=DunderChecker("__nat__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__nat__"), higher_order_value=False) def __new__(x): ... - @hugr_op(int_op("ior"), effects=[]) + @hugr_op(int_op("ior")) def __or__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: nat) -> nat: ... - @hugr_op(int_op("ipow"), effects=[]) + @hugr_op(int_op("ipow")) def __pow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rand__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) @@ -121,40 +116,40 @@ def __rdivmod__(self: nat, other: nat) -> tuple[nat, nat]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rlshift__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rmod__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __ror__(self: nat, other: nat) -> nat: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __round__(self: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rpow__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rrshift__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("ishr"), effects=[]) + @hugr_op(int_op("ishr")) def __rshift__(self: nat, other: nat) -> nat: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: nat, other: nat) -> nat: ... @custom_function(checker=ReversingChecker()) def __rtruediv__(self: nat, other: nat) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rxor__(self: nat, other: nat) -> nat: ... - @hugr_op(int_op("isub"), effects=[]) + @hugr_op(int_op("isub")) def __sub__(self: nat, other: nat) -> nat: ... @guppy @@ -162,10 +157,10 @@ def __sub__(self: nat, other: nat) -> nat: ... def __truediv__(self: nat, other: nat) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __trunc__(self: nat) -> nat: ... - @hugr_op(int_op("ixor"), effects=[]) + @hugr_op(int_op("ixor")) def __xor__(self: nat, other: nat) -> nat: ... @@ -173,56 +168,51 @@ def __xor__(self: nat, other: nat) -> nat: ... class int: """A 64-bit signed integer.""" - @hugr_op( - int_op("iabs"), # TODO: Maybe wrong? (signed vs unsigned!) - effects=[], - ) + @hugr_op(int_op("iabs")) # TODO: Maybe wrong? (signed vs unsigned!) def __abs__(self: int) -> int: ... - @hugr_op(int_op("iadd"), effects=[]) + @hugr_op(int_op("iadd"), effects=[]) # Annotation done early for use in tests def __add__(self: int, other: int) -> int: ... - @hugr_op(int_op("iand"), effects=[]) + @hugr_op(int_op("iand")) def __and__(self: int, other: int) -> int: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: int) -> bool: return self != 0 - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __ceil__(self: int) -> int: ... - # Panics if other == 0 - @hugr_op(int_op("idivmod_s"), effects=[Effect.ANY]) + @hugr_op(int_op("idivmod_s")) def __divmod__(self: int, other: int) -> tuple[int, int]: ... - @custom_function(BoolOpCompiler(int_op("ieq")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ieq"))) def __eq__(self: int, other: int) -> bool: ... - @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION), effects=[]) + @hugr_op(int_op("convert_s", hugr.std.int.CONVERSIONS_EXTENSION)) def __float__(self: int) -> float: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __floor__(self: int) -> int: ... - # Panics if other == 0 - @hugr_op(int_op("idiv_s"), effects=[Effect.ANY]) + @hugr_op(int_op("idiv_s")) def __floordiv__(self: int, other: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ige_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ige_s"))) def __ge__(self: int, other: int) -> bool: ... - @custom_function(BoolOpCompiler(int_op("igt_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("igt_s"))) def __gt__(self: int, other: int) -> bool: ... @custom_function(NoopCompiler()) def __int__(self: int) -> int: ... - @hugr_op(int_op("inot"), effects=[]) + @hugr_op(int_op("inot")) def __invert__(self: int) -> int: ... - @custom_function(BoolOpCompiler(int_op("ile_s")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ile_s"))) def __le__(self: int, other: int) -> bool: ... @hugr_op(int_op("ishl")) # TODO: RHS is unsigned @@ -231,30 +221,28 @@ def __lshift__(self: int, other: int) -> int: ... @custom_function(BoolOpCompiler(int_op("ilt_s"))) def __lt__(self: int, other: int) -> bool: ... - @hugr_op(int_op("imod_s"), effects=[]) + @hugr_op(int_op("imod_s")) def __mod__(self: int, other: int) -> int: ... - @hugr_op(int_op("imul"), effects=[]) + @hugr_op(int_op("imul")) def __mul__(self: int, other: int) -> int: ... - @hugr_op(int_op("is_to_u"), effects=[]) + @hugr_op(int_op("is_to_u")) def __nat__(self: int) -> nat: ... - @custom_function(BoolOpCompiler(int_op("ine")), effects=[]) + @custom_function(BoolOpCompiler(int_op("ine"))) def __ne__(self: int, other: int) -> bool: ... - @hugr_op(int_op("ineg"), effects=[]) + @hugr_op(int_op("ineg")) def __neg__(self: int) -> int: ... - @custom_function( - checker=DunderChecker("__int__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__int__"), higher_order_value=False) def __new__(x): ... - @hugr_op(int_op("ior"), effects=[]) + @hugr_op(int_op("ior")) def __or__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: int) -> int: ... @guppy @@ -267,13 +255,13 @@ def __pow__(self: int, exponent: int) -> int: ) return self.__pow_impl(exponent) - @hugr_op(int_op("ipow"), effects=[]) # Exponent is treated as unsigned + @hugr_op(int_op("ipow")) def __pow_impl(self: int, exponent: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rand__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -282,23 +270,19 @@ def __rdivmod__(self: int, other: int) -> tuple[int, int]: ... @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: int, other: int) -> int: ... - @custom_function( - checker=ReversingChecker(), # TODO: RHS is unsigned - effects=[], - ) + @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rlshift__(self: int, other: int) -> int: ... - # What if other==0 ? @custom_function(checker=ReversingChecker()) def __rmod__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __ror__(self: int, other: int) -> int: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __round__(self: int) -> int: ... @custom_function(checker=ReversingChecker()) @@ -307,33 +291,30 @@ def __rpow__(self: int, other: int) -> int: ... @custom_function(checker=ReversingChecker()) # TODO: RHS is unsigned def __rrshift__(self: int, other: int) -> int: ... - @hugr_op( - int_op("ishr"), # TODO: RHS is unsigned - effects=[], - ) + @hugr_op(int_op("ishr")) # TODO: RHS is unsigned def __rshift__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: int, other: int) -> int: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rtruediv__(self: int, other: int) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rxor__(self: int, other: int) -> int: ... - @hugr_op(int_op("isub"), effects=[]) + @hugr_op(int_op("isub")) def __sub__(self: int, other: int) -> int: ... - @guppy(effects=[]) + @guppy @no_type_check def __truediv__(self: int, other: int) -> float: return float(self) / float(other) - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __trunc__(self: int) -> int: ... - @hugr_op(int_op("ixor"), effects=[]) + @hugr_op(int_op("ixor")) def __xor__(self: int, other: int) -> int: ... @@ -341,128 +322,124 @@ def __xor__(self: int, other: int) -> int: ... class float: """An IEEE754 double-precision floating point value.""" - @hugr_op(float_op("fabs"), effects=[]) + @hugr_op(float_op("fabs")) def __abs__(self: float) -> float: ... - @hugr_op(float_op("fadd"), effects=[]) + @hugr_op(float_op("fadd")) def __add__(self: float, other: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __bool__(self: float) -> bool: return self != 0.0 - @hugr_op(float_op("fceil"), effects=[]) + @hugr_op(float_op("fceil")) def __ceil__(self: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __divmod__(self: float, other: float) -> tuple[float, float]: return self // other, self.__mod__(other) - @custom_function(BoolOpCompiler(float_op("feq")), effects=[]) + @custom_function(BoolOpCompiler(float_op("feq"))) def __eq__(self: float, other: float) -> bool: ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __float__(self: float) -> float: ... - @hugr_op(float_op("ffloor"), effects=[]) + @hugr_op(float_op("ffloor")) def __floor__(self: float) -> float: ... - @guppy(effects=[]) + @guppy @no_type_check def __floordiv__(self: float, other: float) -> float: return (self / other).__floor__() - @custom_function(BoolOpCompiler(float_op("fge")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fge"))) def __ge__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("fgt")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fgt"))) def __gt__(self: float, other: float) -> bool: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_s", hugr.std.int.CONVERSIONS_EXTENSION), - ), - effects=[], + ) ) def __int__(self: float) -> int: ... - @custom_function(BoolOpCompiler(float_op("fle")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fle"))) def __le__(self: float, other: float) -> bool: ... - @custom_function(BoolOpCompiler(float_op("flt")), effects=[]) + @custom_function(BoolOpCompiler(float_op("flt"))) def __lt__(self: float, other: float) -> bool: ... - @guppy(effects=[]) + @guppy @no_type_check def __mod__(self: float, other: float) -> float: return self - (self // other) * other - @hugr_op(float_op("fmul"), effects=[]) + @hugr_op(float_op("fmul")) def __mul__(self: float, other: float) -> float: ... @custom_function( UnwrapOpCompiler( # Use `int_op` to instantiate type arg with 64-bit integer. int_op("trunc_u", hugr.std.int.CONVERSIONS_EXTENSION), - ), - effects=[], + ) ) def __nat__(self: float) -> nat: ... - @custom_function(BoolOpCompiler(float_op("fne")), effects=[]) + @custom_function(BoolOpCompiler(float_op("fne"))) def __ne__(self: float, other: float) -> bool: ... - @hugr_op(float_op("fneg"), effects=[]) + @hugr_op(float_op("fneg")) def __neg__(self: float) -> float: ... - @custom_function( - checker=DunderChecker("__float__"), higher_order_value=False, effects=[] - ) + @custom_function(checker=DunderChecker("__float__"), higher_order_value=False) def __new__(x): ... - @custom_function(NoopCompiler(), effects=[]) + @custom_function(NoopCompiler()) def __pos__(self: float) -> float: ... - @hugr_op(float_op("fpow"), effects=[]) # TODO + @hugr_op(float_op("fpow")) # TODO def __pow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __radd__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rdivmod__(self: float, other: float) -> tuple[float, float]: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rfloordiv__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmod__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rmul__(self: float, other: float) -> float: ... - @hugr_op(float_op("fround"), effects=[]) # TODO + @hugr_op(float_op("fround")) # TODO def __round__(self: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rpow__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rsub__(self: float, other: float) -> float: ... - @custom_function(checker=ReversingChecker(), effects=[]) + @custom_function(checker=ReversingChecker()) def __rtruediv__(self: float, other: float) -> float: ... - @hugr_op(float_op("fsub"), effects=[]) + @hugr_op(float_op("fsub")) def __sub__(self: float, other: float) -> float: ... - @hugr_op(float_op("fdiv"), effects=[]) + @hugr_op(float_op("fdiv")) def __truediv__(self: float, other: float) -> float: ... - @hugr_op(unsupported_op("trunc_s"), effects=[]) # TODO `trunc_s` returns an option + @hugr_op(unsupported_op("trunc_s")) # TODO `trunc_s` returns an option def __trunc__(self: float) -> float: ... diff --git a/guppylang/src/guppylang/std/qsystem/__init__.py b/guppylang/src/guppylang/std/qsystem/__init__.py index 19cf46f1a..b2ab03655 100644 --- a/guppylang/src/guppylang/std/qsystem/__init__.py +++ b/guppylang/src/guppylang/std/qsystem/__init__.py @@ -256,14 +256,14 @@ class Measurement: """Represents the result of a lazy measurement which needs to be explicitly read before being used.""" - @custom_function(compiler=ReadFutureBoolCompiler(), effects=[]) + @custom_function(compiler=ReadFutureBoolCompiler()) @no_type_check def read(self: "Measurement" @ owned) -> bool: """Read the measurement result, consuming it. Blocks until the result is available if the measurement hasn't been performed yet since being requested. """ - @guppy(effects=[]) + @guppy @no_type_check def __consume_as_bool__(self: "Measurement" @ owned) -> bool: return self.read() diff --git a/guppylang/src/guppylang/std/quantum/__init__.py b/guppylang/src/guppylang/std/quantum/__init__.py index 0c78b305b..38599c832 100644 --- a/guppylang/src/guppylang/std/quantum/__init__.py +++ b/guppylang/src/guppylang/std/quantum/__init__.py @@ -26,12 +26,12 @@ class qubit: @no_type_check def __new__() -> "qubit": ... - @guppy # not pure: this is measure+free + @guppy @no_type_check def measure(self: "qubit" @ owned) -> bool: return measure(self) - @guppy(effects=[]) + @guppy @no_type_check def project_z(self: "qubit") -> bool: return project_z(self) @@ -49,7 +49,7 @@ def maybe_qubit() -> Option[qubit]: if allocation succeeds or `nothing` if it fails.""" -@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("H"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def h(q: qubit) -> None: r"""Hadamard gate command @@ -63,7 +63,7 @@ def h(q: qubit) -> None: """ -@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CZ"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cz(control: qubit, target: qubit) -> None: r"""Controlled-Z gate command. @@ -83,7 +83,7 @@ def cz(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CY"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cy(control: qubit, target: qubit) -> None: r"""Controlled-Y gate command. @@ -103,7 +103,7 @@ def cy(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("CX"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def cx(control: qubit, target: qubit) -> None: r"""Controlled-X gate command. @@ -123,7 +123,7 @@ def cx(control: qubit, target: qubit) -> None: """ -@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("T"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def t(q: qubit) -> None: r"""T gate. @@ -138,7 +138,7 @@ def t(q: qubit) -> None: """ -@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("S"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def s(q: qubit) -> None: r"""S gate. @@ -153,7 +153,7 @@ def s(q: qubit) -> None: """ -@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("V"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def v(q: qubit) -> None: r"""V gate. @@ -168,7 +168,7 @@ def v(q: qubit) -> None: """ -@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("X"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def x(q: qubit) -> None: r"""X gate. @@ -183,7 +183,7 @@ def x(q: qubit) -> None: """ -@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Y"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def y(q: qubit) -> None: r"""Y gate. @@ -198,7 +198,7 @@ def y(q: qubit) -> None: """ -@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Z"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def z(q: qubit) -> None: r"""Z gate. @@ -213,7 +213,7 @@ def z(q: qubit) -> None: """ -@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Tdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def tdg(q: qubit) -> None: r"""Tdg gate. @@ -228,7 +228,7 @@ def tdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Sdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def sdg(q: qubit) -> None: r"""Sdg gate. @@ -243,7 +243,7 @@ def sdg(q: qubit) -> None: """ -@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Vdg"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def vdg(q: qubit) -> None: r"""Vdg gate. @@ -258,7 +258,7 @@ def vdg(q: qubit) -> None: """ -@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Rz"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def rz(q: qubit, angle: angle) -> None: r"""Rz gate. @@ -274,7 +274,7 @@ def rz(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Rx"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def rx(q: qubit, angle: angle) -> None: r"""Rx gate. @@ -289,7 +289,7 @@ def rx(q: qubit, angle: angle) -> None: """ -@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@custom_function(RotationCompiler("Ry"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def ry(q: qubit, angle: angle) -> None: r"""Ry gate. @@ -304,9 +304,7 @@ def ry(q: qubit, angle: angle) -> None: """ -@custom_function( - RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary, effects=[] -) +@custom_function(RotationCompiler("CRz"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def crz(control: qubit, target: qubit, angle: angle) -> None: r"""Controlled-Rz gate command. @@ -326,7 +324,7 @@ def crz(control: qubit, target: qubit, angle: angle) -> None: """ -@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary, effects=[]) +@hugr_op(quantum_op("Toffoli"), unitary_flags=UnitaryFlags.Unitary) @no_type_check def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: r"""A Toffoli gate command. Also sometimes known as a CCX gate. @@ -350,7 +348,7 @@ def toffoli(control1: qubit, control2: qubit, target: qubit) -> None: """ -@custom_function(InoutMeasureCompiler(), effects=[]) +@custom_function(InoutMeasureCompiler()) @no_type_check def project_z(q: qubit) -> bool: """Project a single qubit into the Z-basis (a non-destructive measurement).""" @@ -368,7 +366,7 @@ def measure(q: qubit @ owned) -> bool: """Measure a single qubit destructively.""" -@hugr_op(quantum_op("Reset"), effects=[]) +@hugr_op(quantum_op("Reset")) @no_type_check def reset(q: qubit) -> None: """Reset a single qubit to the :math:`|0\rangle` state.""" @@ -377,7 +375,7 @@ def reset(q: qubit) -> None: N = guppy.nat_var("N") -@guppy # This does N calls to QFree, so it is not pure +@guppy @no_type_check def measure_array(qubits: array[qubit, N] @ owned) -> array[bool, N]: """Measure an array of qubits, returning an array of bools.""" @@ -397,7 +395,7 @@ def discard_array(qubits: array[qubit, N] @ owned) -> None: # -------NON-PRIMITIVE------- -@guppy(effects=[]) +@guppy @no_type_check def ch(control: qubit, target: qubit) -> None: r"""Controlled-H gate command. diff --git a/guppylang/src/guppylang/std/quantum/functional.py b/guppylang/src/guppylang/std/quantum/functional.py index 756348b1e..58cebf98b 100644 --- a/guppylang/src/guppylang/std/quantum/functional.py +++ b/guppylang/src/guppylang/std/quantum/functional.py @@ -15,7 +15,7 @@ from guppylang.std.quantum import qubit -@guppy(effects=[]) +@guppy @no_type_check def h(q: qubit @ owned) -> qubit: """Functional Hadamard gate command.""" @@ -23,7 +23,7 @@ def h(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CZ gate command.""" @@ -31,7 +31,7 @@ def cz(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CX gate command.""" @@ -39,7 +39,7 @@ def cx(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional CY gate command.""" @@ -47,7 +47,7 @@ def cy(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: return control, target -@guppy(effects=[]) +@guppy @no_type_check def t(q: qubit @ owned) -> qubit: """Functional T gate command.""" @@ -55,7 +55,7 @@ def t(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def s(q: qubit @ owned) -> qubit: """Functional S gate command.""" @@ -63,7 +63,7 @@ def s(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def v(q: qubit @ owned) -> qubit: """Functional V gate command.""" @@ -71,7 +71,7 @@ def v(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def x(q: qubit @ owned) -> qubit: """Functional X gate command.""" @@ -79,7 +79,7 @@ def x(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def y(q: qubit @ owned) -> qubit: """Functional Y gate command.""" @@ -87,7 +87,7 @@ def y(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def z(q: qubit @ owned) -> qubit: """Functional Z gate command.""" @@ -95,7 +95,7 @@ def z(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def tdg(q: qubit @ owned) -> qubit: """Functional Tdg gate command.""" @@ -103,7 +103,7 @@ def tdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def sdg(q: qubit @ owned) -> qubit: """Functional Sdg gate command.""" @@ -111,7 +111,7 @@ def sdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def vdg(q: qubit @ owned) -> qubit: """Functional Vdg gate command.""" @@ -119,7 +119,7 @@ def vdg(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def rz(q: qubit @ owned, angle: angle) -> qubit: """Functional Rz gate command.""" @@ -127,7 +127,7 @@ def rz(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def rx(q: qubit @ owned, angle: angle) -> qubit: """Functional Rx gate command.""" @@ -135,7 +135,7 @@ def rx(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def ry(q: qubit @ owned, angle: angle) -> qubit: """Functional Ry gate command.""" @@ -143,7 +143,7 @@ def ry(q: qubit @ owned, angle: angle) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def crz( control: qubit @ owned, target: qubit @ owned, angle: angle @@ -153,7 +153,7 @@ def crz( return control, target -@guppy(effects=[]) +@guppy @no_type_check def toffoli( control1: qubit @ owned, control2: qubit @ owned, target: qubit @ owned @@ -163,7 +163,7 @@ def toffoli( return control1, control2, target -@guppy(effects=[]) +@guppy @no_type_check def reset(q: qubit @ owned) -> qubit: """Functional Reset command.""" @@ -171,7 +171,7 @@ def reset(q: qubit @ owned) -> qubit: return q -@guppy(effects=[]) +@guppy @no_type_check def project_z(q: qubit @ owned) -> tuple[qubit, bool]: """Functional project_z command.""" @@ -182,7 +182,7 @@ def project_z(q: qubit @ owned) -> tuple[qubit, bool]: # -------NON-PRIMITIVE------- -@guppy(effects=[]) +@guppy @no_type_check def ch(control: qubit @ owned, target: qubit @ owned) -> tuple[qubit, qubit]: """Functional Controlled-H gate command.""" diff --git a/tests/error/modifier_errors/higher_order.err b/tests/error/modifier_errors/higher_order.err index d9e326b4d..c472124ba 100644 --- a/tests/error/modifier_errors/higher_order.err +++ b/tests/error/modifier_errors/higher_order.err @@ -1,8 +1,8 @@ -Error: Dagger constraint violation (at $FILE:11:4) +Error: Dagger constraint violation (at $FILE:10:4) | - 9 | @guppy(dagger=True) -10 | def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: -11 | f(q) + 8 | @guppy(dagger=True) + 9 | def test_ho(f: Callable[[qubit], None], q: qubit) -> None: +10 | f(q) | ^^^^ This function cannot be called in a dagger context Guppy compilation failed due to 1 previous error diff --git a/tests/error/modifier_errors/higher_order.py b/tests/error/modifier_errors/higher_order.py index a36e8244a..e32fc884c 100644 --- a/tests/error/modifier_errors/higher_order.py +++ b/tests/error/modifier_errors/higher_order.py @@ -1,13 +1,12 @@ from guppylang.decorator import guppy from guppylang.std.builtins import dagger -from guppylang.std.effects import effects from guppylang.std.quantum import qubit, h, discard from collections.abc import Callable # f would need a flag to be used in dagger context, but no way to specify that yet @guppy(dagger=True) -def test_ho(f: Callable[[qubit], None] @ effects(), q: qubit) -> None: +def test_ho(f: Callable[[qubit], None], q: qubit) -> None: f(q) diff --git a/tests/error/poly_errors/non_linear2.py b/tests/error/poly_errors/non_linear2.py index 005f85fba..9efcf0f5b 100644 --- a/tests/error/poly_errors/non_linear2.py +++ b/tests/error/poly_errors/non_linear2.py @@ -10,7 +10,7 @@ # Pending https://github.com/Quantinuum/guppylang/issues/1760 we need to explicitly # declare the effects of `x` @guppy.declare -def foo(x: Callable[[T], T] @ effects()) -> None: ... +def foo(x: Callable[[T], T]) -> None: ... @guppy diff --git a/tests/error/struct_errors/constructor_missing_arg.err b/tests/error/struct_errors/constructor_missing_arg.err index 9695292e0..1a673d9b8 100644 --- a/tests/error/struct_errors/constructor_missing_arg.err +++ b/tests/error/struct_errors/constructor_missing_arg.err @@ -5,6 +5,6 @@ Error: Not enough arguments (at $FILE:11:12) 11 | MyStruct() | ^^ Missing argument (expected 1, got 0) -Note: Function signature is `int -[]-> MyStruct` +Note: Function signature is `int -> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_too_many_args.err b/tests/error/struct_errors/constructor_too_many_args.err index e993ba76b..2e5f65d47 100644 --- a/tests/error/struct_errors/constructor_too_many_args.err +++ b/tests/error/struct_errors/constructor_too_many_args.err @@ -5,6 +5,6 @@ Error: Too many arguments (at $FILE:11:16) 11 | MyStruct(1, 2, 3) | ^^^^ Unexpected arguments (expected 1, got 3) -Note: Function signature is `int -[]-> MyStruct` +Note: Function signature is `int -> MyStruct` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_left.err b/tests/error/type_errors/and_not_bool_left.err index 01e5c670d..cc8839a67 100644 --- a/tests/error/type_errors/and_not_bool_left.err +++ b/tests/error/type_errors/and_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_right.err b/tests/error/type_errors/and_not_bool_right.err index 9af5981d3..52586da05 100644 --- a/tests/error/type_errors/and_not_bool_right.err +++ b/tests/error/type_errors/and_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:17) 7 | return x and y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_expr_not_bool.err b/tests/error/type_errors/if_expr_not_bool.err index 6b6dbdfe2..ffb3fb0f9 100644 --- a/tests/error/type_errors/if_expr_not_bool.err +++ b/tests/error/type_errors/if_expr_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return 1 if x else 0 | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_not_bool.err b/tests/error/type_errors/if_not_bool.err index 96ff71a86..b52e7b368 100644 --- a/tests/error/type_errors/if_not_bool.err +++ b/tests/error/type_errors/if_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:7) 7 | if x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_not_bool.err b/tests/error/type_errors/not_not_bool.err index 8d9ad86b2..ab16adf98 100644 --- a/tests/error/type_errors/not_not_bool.err +++ b/tests/error/type_errors/not_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:15) 7 | return not x | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_left.err b/tests/error/type_errors/or_not_bool_left.err index bd8ce6c86..c17142398 100644 --- a/tests/error/type_errors/or_not_bool_left.err +++ b/tests/error/type_errors/or_not_bool_left.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:11) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_right.err b/tests/error/type_errors/or_not_bool_right.err index f4495645b..4769de617 100644 --- a/tests/error/type_errors/or_not_bool_right.err +++ b/tests/error/type_errors/or_not_bool_right.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:16) 7 | return x or y | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/while_not_bool.err b/tests/error/type_errors/while_not_bool.err index 1dc04396f..13469fd99 100644 --- a/tests/error/type_errors/while_not_bool.err +++ b/tests/error/type_errors/while_not_bool.err @@ -5,6 +5,6 @@ Error: Not truthy (at $FILE:7:10) 7 | while x: | ^ Expression of type `NonBool` is not truthy -Help: Implement missing method: `__bool__: NonBool -[]-> bool` +Help: Implement missing method: `__bool__: NonBool -> bool` Guppy compilation failed due to 1 previous error diff --git a/tests/integration/notebooks/misc_notebook_tests.ipynb b/tests/integration/notebooks/misc_notebook_tests.ipynb index 518c17ead..588d46b8b 100644 --- a/tests/integration/notebooks/misc_notebook_tests.ipynb +++ b/tests/integration/notebooks/misc_notebook_tests.ipynb @@ -137,7 +137,7 @@ "16 | return MyStruct()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -[]-> MyStruct`\n", + "Note: Function signature is `int -> MyStruct`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] @@ -243,7 +243,7 @@ "16 | return MyEnum.Var()\n", " | ^^ Missing argument (expected 1, got 0)\n", "\n", - "Note: Function signature is `int -[]-> MyEnum`\n", + "Note: Function signature is `int -> MyEnum`\n", "\n", "Guppy compilation failed due to 1 previous error\n" ] diff --git a/tests/integration/test_enum.py b/tests/integration/test_enum.py index b5ff65ed5..b302ba621 100644 --- a/tests/integration/test_enum.py +++ b/tests/integration/test_enum.py @@ -21,7 +21,6 @@ """ from guppylang import guppy -from guppylang.std.effects import effects from tests.util import compile_guppy from typing import Generic @@ -248,7 +247,7 @@ class Enum(Generic[T]): # pyright: ignore[reportInvalidTypeForm] VariantA = {"x": T} @guppy - def factory(mk_enum: Callable[[int], Enum[int]] @ effects(), x: int) -> Enum[int]: + def factory(mk_enum: Callable[[int], Enum[int]], x: int) -> Enum[int]: return mk_enum(x) @guppy diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index 42c85699d..55616cec6 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -5,8 +5,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from guppylang.std.effects import effects - def test_basic_defs(validate): @guppy.struct @@ -134,12 +132,8 @@ def test_higher_order(validate): class Struct(Generic[T]): x: T - # Pending https://github.com/Quantinuum/guppylang/issues/1760 - # we must explicitly state the effects of `mk_struct` @guppy - def factory( - mk_struct: "Callable[[int], Struct[int]] @ effects()", x: int - ) -> Struct[int]: + def factory(mk_struct: "Callable[[int], Struct[int]]", x: int) -> Struct[int]: return mk_struct(x) @guppy diff --git a/tests/integration/tracing/test_enum.py b/tests/integration/tracing/test_enum.py index ecc5e8ec0..dd3a16412 100644 --- a/tests/integration/tracing/test_enum.py +++ b/tests/integration/tracing/test_enum.py @@ -2,7 +2,6 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.effects import effects def test_create(validate): @@ -108,7 +107,7 @@ class MyEnum: VariantA = {"x": int} @guppy.comptime - def test() -> Callable[[int], MyEnum] @ effects(): + def test() -> Callable[[int], MyEnum]: return MyEnum.VariantA validate(test.compile_function()) diff --git a/tests/integration/tracing/test_struct.py b/tests/integration/tracing/test_struct.py index 77e1d647f..f9cb545df 100644 --- a/tests/integration/tracing/test_struct.py +++ b/tests/integration/tracing/test_struct.py @@ -2,7 +2,6 @@ from typing import Generic from guppylang.decorator import guppy -from guppylang.std.effects import effects def test_create(run_int_fn): @@ -115,7 +114,7 @@ class S: x: int @guppy.comptime - def test() -> Callable[[int], S] @ effects(): + def test() -> Callable[[int], S]: return S validate(test.compile_function())