diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 3e0a0b92..5d35abd6 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -1,4 +1,5 @@ import ast +import copy import itertools from collections.abc import Iterator from typing import NamedTuple @@ -270,7 +271,9 @@ def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.Name: # assignment statement and replace the expression with `x`. if not isinstance(node.target, ast.Name): raise InternalGuppyError(f"Unexpected assign target: {node.target}") - assign = ast.Assign(targets=[node.target], value=self.visit(node.value)) + assign = ast.Assign( + targets=[copy.deepcopy(node.target)], value=self.visit(node.value) + ) set_location_from(assign, node) self.bb.statements.append(assign) return node.target diff --git a/guppylang/checker/cfg_checker.py b/guppylang/checker/cfg_checker.py index b61ce51f..ca7f7509 100644 --- a/guppylang/checker/cfg_checker.py +++ b/guppylang/checker/cfg_checker.py @@ -14,6 +14,7 @@ from guppylang.cfg.cfg import CFG, BaseCFG from guppylang.checker.core import Context, Globals, Locals, Variable from guppylang.checker.expr_checker import ExprSynthesizer, to_bool +from guppylang.checker.linearity_checker import check_cfg_linearity from guppylang.checker.stmt_checker import StmtChecker from guppylang.error import GuppyError from guppylang.tys.ty import Type @@ -84,7 +85,7 @@ def check_cfg( while len(queue) > 0: pred, num_output, bb = queue.popleft() input_row = [ - Variable(v.name, v.ty, v.defined_at, None) + Variable(v.name, v.ty, v.defined_at) for v in pred.sig.output_rows[num_output] ] @@ -114,6 +115,10 @@ def check_cfg( checked_cfg.maybe_ass_before = { compiled[bb]: cfg.maybe_ass_before[bb] for bb in cfg.bbs } + + # Finally, run the linearity check + check_cfg_linearity(checked_cfg) + return checked_cfg @@ -160,29 +165,6 @@ def check_bb( ) raise GuppyError(f"Variable `{x}` is not defined", use_bb.vars.used[x]) - # We have to check that used linear variables are not being outputted - if x in ctx.locals: - var = ctx.locals[x] - if var.ty.linear and var.used: - raise GuppyError( - f"Variable `{x}` with linear type `{var.ty}` was " - "already used (at {0})", - cfg.live_before[succ][x].vars.used[x], - [var.used], - ) - - # On the other hand, unused linear variables *must* be outputted - for x, var in ctx.locals.items(): - if var.ty.linear and not var.used and x not in cfg.live_before[succ]: - # TODO: This should be "Variable x with linear type ty is not - # used in {bb}". But for this we need a way to associate BBs with - # source locations. - raise GuppyError( - f"Variable `{x}` with linear type `{var.ty}` is " - "not used on all control-flow paths", - var.defined_at, - ) - # Finally, we need to compute the signature of the basic block outputs = [ [ctx.locals[x] for x in cfg.live_before[succ] if x in ctx.locals] diff --git a/guppylang/checker/core.py b/guppylang/checker/core.py index e0c2a3cb..95fdd450 100644 --- a/guppylang/checker/core.py +++ b/guppylang/checker/core.py @@ -37,14 +37,13 @@ ) -@dataclass +@dataclass(frozen=True) class Variable: """Class holding data associated with a local variable.""" name: str ty: Type defined_at: AstNode | None - used: AstNode | None PyScope = dict[str, Any] diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index 29b37869..541c23a3 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -31,7 +31,6 @@ AstVisitor, breaks_in_loop, get_type_opt, - name_nodes_in_ast, return_nodes_in_ast, with_loc, with_type, @@ -333,14 +332,6 @@ def visit_Name(self, node: ast.Name) -> tuple[ast.Name, Type]: x = node.id if x in self.ctx.locals: var = self.ctx.locals[x] - if var.ty.linear and var.used is not None: - raise GuppyError( - f"Variable `{x}` with linear type `{var.ty}` was " - "already used (at {0})", - node, - [var.used], - ) - var.used = node return with_loc(node, LocalName(id=x)), var.ty elif x in self.ctx.globals: # Look-up what kind of definition it is @@ -874,34 +865,14 @@ def synthesize_comprehension( """Helper function to synthesise the element type of a list comprehension.""" from guppylang.checker.stmt_checker import StmtChecker - def check_linear_use_from_outer_scope(expr: ast.expr, locals: Locals) -> None: - """Checks if an expression uses a linear variable from an outer scope. - - Since the expression is executed multiple times in the inner scope, this would - mean that the outer linear variable is used multiple times, which is not - allowed. - """ - for name in name_nodes_in_ast(expr): - x = name.id - if x in locals and x not in locals.vars: - var = locals[x] - if var.ty.linear: - raise GuppyTypeError( - f"Variable `{x}` with linear type `{var.ty}` would be used " - "multiple times when evaluating this comprehension", - name, - ) - # If there are no more generators left, we can check the list element if not gens: node.elt, elt_ty = ExprSynthesizer(ctx).synthesize(node.elt) - check_linear_use_from_outer_scope(node.elt, ctx.locals) return node, elt_ty # Check the iterator in the outer context gen, *gens = gens gen.iter_assign = StmtChecker(ctx).visit_Assign(gen.iter_assign) - check_linear_use_from_outer_scope(gen.iter_assign.value, ctx.locals) # The rest is checked in a new nested context to ensure that variables don't escape # their scope @@ -915,43 +886,15 @@ def check_linear_use_from_outer_scope(expr: ast.expr, locals: Locals) -> None: gen.iter, iter_ty = expr_sth.visit_Name(gen.iter) gen.iter = with_type(iter_ty, gen.iter) - # `if` guards are generally not allowed when we're iterating over linear variables. - # The only exception is if all linear variables are already consumed by the first - # guard - if gen.ifs: - gen.ifs[0], _ = expr_sth.synthesize(gen.ifs[0]) - - # Now, check if there are linear iteration variables that have not been used by - # the first guard - for target in name_nodes_in_ast(gen.next_assign.targets[0]): - var = inner_ctx.locals[target.id] - if var.ty.linear and not var.used and gen.ifs: - raise GuppyTypeError( - f"Variable `{var.name}` with linear type `{var.ty}` is not used on " - "all control-flow paths of the list comprehension", - target, - ) - - # Now, we can properly check all guards - for i in range(len(gen.ifs)): - gen.ifs[i], if_ty = expr_sth.synthesize(gen.ifs[i]) - gen.ifs[i], _ = to_bool(gen.ifs[i], if_ty, inner_ctx) - check_linear_use_from_outer_scope(gen.ifs[i], inner_locals) + # Check `if` guards + for i in range(len(gen.ifs)): + gen.ifs[i], if_ty = expr_sth.synthesize(gen.ifs[i]) + gen.ifs[i], _ = to_bool(gen.ifs[i], if_ty, inner_ctx) # Check remaining generators node, elt_ty = synthesize_comprehension(node, gens, inner_ctx) - # We have to make sure that all linear variables that were introduced in this scope - # have been used - for x, var in inner_ctx.locals.vars.items(): - if var.ty.linear and not var.used: - raise GuppyTypeError( - f"Variable `{x}` with linear type `{var.ty}` is not used", - var.defined_at, - ) - # The iter finalizer is again checked in the outer context - ctx.locals[gen.iter.id].used = None gen.iterend, iterend_ty = ExprSynthesizer(ctx).synthesize(gen.iterend) gen.iterend = with_type(iterend_ty, gen.iterend) return node, elt_ty diff --git a/guppylang/checker/func_checker.py b/guppylang/checker/func_checker.py index 7a979341..84f2f5d8 100644 --- a/guppylang/checker/func_checker.py +++ b/guppylang/checker/func_checker.py @@ -33,7 +33,7 @@ def check_global_func_def( cfg = CFGBuilder().build(func_def.body, returns_none, globals) inputs = [ - Variable(x, ty, loc, None) + Variable(x, ty, loc) for x, ty, loc in zip(ty.input_names, ty.inputs, args, strict=True) ] return check_cfg(cfg, inputs, ty.output, globals) @@ -87,7 +87,7 @@ def check_nested_func_def( # Construct inputs for checking the body CFG inputs = list(captured.values()) + [ - Variable(x, ty, func_def.args.args[i], None) + Variable(x, ty, func_def.args.args[i]) for i, (x, ty) in enumerate( zip(func_ty.input_names, func_ty.inputs, strict=True) ) @@ -111,7 +111,7 @@ def check_nested_func_def( ) else: # Otherwise, we treat it like a local name - inputs.append(Variable(func_def.name, func_def.ty, func_def, None)) + inputs.append(Variable(func_def.name, func_def.ty, func_def)) checked_cfg = check_cfg(cfg, inputs, func_ty.output, globals) checked_def = CheckedNestedFunctionDef( diff --git a/guppylang/checker/linearity_checker.py b/guppylang/checker/linearity_checker.py new file mode 100644 index 00000000..972cbd46 --- /dev/null +++ b/guppylang/checker/linearity_checker.py @@ -0,0 +1,230 @@ +"""Linearity checking + +Linearity checking across control-flow is done by the `CFGChecker`. +""" + +import ast +from collections.abc import Generator, Iterable +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from guppylang.ast_util import get_type, name_nodes_in_ast +from guppylang.checker.core import Locals, Variable +from guppylang.error import GuppyError, GuppyTypeError +from guppylang.nodes import DesugaredGenerator, DesugaredListComp, LocalName + +if TYPE_CHECKING: + from guppylang.checker.cfg_checker import CheckedBB, CheckedCFG + + +class Scope(Locals): + """Scoped collection of assigned variables indexed by name. + + Keeps track of which variables have already been used. + """ + + parent_scope: "Scope | None" + used_local: dict[str, ast.Name] + used_parent: dict[str, ast.Name] + + def __init__(self, assigned: Iterable[Variable], parent: "Scope | None" = None): + self.used_local = {} + self.used_parent = {} + super().__init__({var.name: var for var in assigned}, parent) + + def used(self, x: str) -> ast.Name | None: + """Checks whether a variable has already been used.""" + if x in self.vars: + return self.used_local.get(x, None) + assert self.parent_scope is not None + return self.parent_scope.used(x) + + def use(self, x: str, node: ast.Name) -> None: + """Records a use of a variable. + + Works for local variables in the current scope as well as variables in any + parent scope. + """ + if x in self.vars: + self.used_local[x] = node + else: + assert self.parent_scope is not None + assert x in self.parent_scope + self.used_parent[x] = node + self.parent_scope.use(x, node) + + def assign(self, var: Variable) -> None: + """Records an assignment of a variable.""" + x = var.name + self.vars[x] = var + if x in self.used_local: + self.used_local.pop(x) + + +class BBLinearityChecker(ast.NodeVisitor): + """AST visitor that checks linearity for a single basic block.""" + + scope: Scope + + def check(self, bb: "CheckedBB") -> Scope: + self.scope = Scope(bb.sig.input_row) + for stmt in bb.statements: + self.visit(stmt) + if bb.branch_pred: + self.visit(bb.branch_pred) + return self.scope + + @contextmanager + def new_scope(self) -> Generator[Scope, None, None]: + scope, new_scope = self.scope, Scope({}, self.scope) + self.scope = new_scope + yield new_scope + self.scope = scope + + def visit_LocalName(self, node: LocalName) -> None: + x = node.id + if x in self.scope: + var = self.scope[x] + if (use := self.scope.used(x)) and var.ty.linear: + raise GuppyError( + f"Variable `{x}` with linear type `{var.ty}` was already used " + "(at {0})", + node, + [use], + ) + self.scope.use(x, node) + + def visit_Assign(self, node: ast.Assign) -> None: + self.visit(node.value) + self._check_assign_targets(node.targets) + + def visit_Expr(self, node: ast.Expr) -> None: + # An expression statement where the return value is discarded + self.visit(node.value) + ty = get_type(node.value) + if ty.linear: + raise GuppyTypeError(f"Value with linear type `{ty}` is not used", node) + + def visit_DesugaredListComp(self, node: DesugaredListComp) -> None: + self._check_comprehension(node, node.generators) + + def _check_assign_targets(self, targets: list[ast.expr]) -> None: + """Helper function to check assignments.""" + # We're not allowed to override an unused linear variable + [target] = targets + for name in name_nodes_in_ast(target): + x = name.id + if x in self.scope and not self.scope.used(x): + var = self.scope[x] + if var.ty.linear: + raise GuppyError( + f"Variable `{x}` with linear type `{var.ty}` is not used", + var.defined_at, + ) + self.scope.assign(Variable(x, get_type(name), name)) + + def _check_comprehension( + self, node: DesugaredListComp, gens: list[DesugaredGenerator] + ) -> None: + """Helper function to recursively check list comprehensions.""" + if not gens: + self.visit(node.elt) + return + + # Check the iterator expression in the current scope + gen, *gens = gens + self.visit(gen.iter_assign.value) + + # The rest is checked in a new nested scope so we can track which variables + # are introduced and used inside the loop + with self.new_scope() as inner_scope: + # In particular, assign the iterator variable in the new scope + self._check_assign_targets(gen.iter_assign.targets) + self.visit(gen.hasnext_assign) + self.visit(gen.next_assign) + + # `if` guards are generally not allowed when we're iterating over linear + # variables. The only exception is if all linear variables are already + # consumed by the first guard + if gen.ifs: + first_if, *other_ifs = gen.ifs + # Check if there are linear iteration variables that have not been used + # by the first guard + self.visit(first_if) + for x, var in self.scope.vars.items(): + # The only exception is the iterator variable since we make sure + # that it is carried through each iteration during Hugr generation + if x == gen.iter.id: + continue + if not self.scope.used(x) and var.ty.linear: + raise GuppyTypeError( + f"Variable `{var.name}` with linear type `{var.ty}` is not " + "used on all control-flow paths of the list comprehension", + var.defined_at, + ) + for expr in other_ifs: + self.visit(expr) + + # Recursively check the remaining generators + self._check_comprehension(node, gens) + + # Check the iter finalizer so we record a final use of the iterator + self.visit(gen.iterend) + + # We have to make sure that all linear variables that were introduced in the + # inner scope have been used + for x, var in inner_scope.vars.items(): + if var.ty.linear and not inner_scope.used(x): + raise GuppyTypeError( + f"Variable `{x}` with linear type `{var.ty}` is not used", + var.defined_at, + ) + + # On the other hand, we have to ensure that no linear variables from the + # outer scope have been used inside the comprehension (they would be used + # multiple times since the comprehension body is executed repeatedly) + for x, use in inner_scope.used_parent.items(): + var = inner_scope[x] + if var.ty.linear: + raise GuppyTypeError( + f"Variable `{x}` with linear type `{var.ty}` would be used " + "multiple times when evaluating this comprehension", + use, + ) + + +def check_cfg_linearity(cfg: "CheckedCFG") -> None: + """Checks whether a CFG satisfies the linearity requirements. + + Raises a user-error if linearity violations are found. + """ + bb_checker = BBLinearityChecker() + for bb in cfg.bbs: + scope = bb_checker.check(bb) + + # We have to check that used linear variables are not being outputted + for succ in bb.successors: + live = cfg.live_before[succ] + for x, use_bb in live.items(): + if x in scope: + var = scope[x] + if var.ty.linear and (use := scope.used(x)): + raise GuppyError( + f"Variable `{x}` with linear type `{var.ty}` was " + "already used (at {0})", + use_bb.vars.used[x], + [use], + ) + + # On the other hand, unused linear variables *must* be outputted + for x, var in scope.vars.items(): + used_later = x in cfg.live_before[succ] + if var.ty.linear and not scope.used(x) and not used_later: + # TODO: This should be "Variable x with linear type ty is not + # used in {bb}". But for this we need a way to associate BBs with + # source locations. + raise GuppyError( + f"Variable `{x}` with linear type `{var.ty}` is " + "not used on all control-flow paths", + var.defined_at, + ) diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index 3025e781..1f7d5050 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -11,7 +11,7 @@ import ast from collections.abc import Sequence -from guppylang.ast_util import AstVisitor, with_loc +from guppylang.ast_util import AstVisitor, with_loc, with_type from guppylang.cfg.bb import BB, BBStatement from guppylang.checker.core import Context, Variable from guppylang.checker.expr_checker import ExprChecker, ExprSynthesizer @@ -51,15 +51,9 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> None: match lhs: # Easiest case is if the LHS pattern is a single variable. case ast.Name(id=x): - # Check if we override an unused linear variable - if x in self.ctx.locals: - var = self.ctx.locals[x] - if var.ty.linear and var.used is None: - raise GuppyError( - f"Variable `{x}` with linear type `{var.ty}` is not used", - var.defined_at, - ) - self.ctx.locals[x] = Variable(x, ty, lhs, None) + # Store the type in the AST + with_type(ty, lhs) + self.ctx.locals[x] = Variable(x, ty, lhs) # The only other thing we support right now are tuples case ast.Tuple(elts=elts): @@ -138,9 +132,7 @@ def visit_NestedFunctionDef(self, node: NestedFunctionDef) -> ast.stmt: raise InternalGuppyError("BB required to check nested function def!") 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, None - ) + self.ctx.locals[func_def.name] = Variable(func_def.name, func_def.ty, func_def) return func_def def visit_If(self, node: ast.If) -> None: diff --git a/guppylang/compiler/cfg_compiler.py b/guppylang/compiler/cfg_compiler.py index 8e538970..f3392ccd 100644 --- a/guppylang/compiler/cfg_compiler.py +++ b/guppylang/compiler/cfg_compiler.py @@ -53,7 +53,7 @@ def compile_bb( dfg = DFContainer( block, { - v.name: PortVariable(v.name, inp.out_port(i), v.defined_at, None) + v.name: PortVariable(v.name, inp.out_port(i), v.defined_at) for (i, v) in enumerate(inputs) }, ) @@ -117,7 +117,7 @@ def insert_return_vars(cfg: CheckedCFG) -> None: correctly outputted. """ return_vars = [ - Variable(return_var(i), ty, None, None) + Variable(return_var(i), ty, None) for i, ty in enumerate(type_to_row(cfg.output_ty)) ] # Before patching, the exit BB shouldn't take any inputs diff --git a/guppylang/compiler/core.py b/guppylang/compiler/core.py index fa1f47cb..3980fc47 100644 --- a/guppylang/compiler/core.py +++ b/guppylang/compiler/core.py @@ -8,7 +8,7 @@ from guppylang.hugr_builder.hugr import DFContainingNode, Hugr, OutPortV -@dataclass +@dataclass(frozen=True) class PortVariable(Variable): """Represents a local variable in a dataflow graph. @@ -22,11 +22,14 @@ def __init__( name: str, port: OutPortV, defined_at: AstNode | None, - used: AstNode | None = None, ) -> None: - super().__init__(name, port.ty, defined_at, used) + super().__init__(name, port.ty, defined_at) object.__setattr__(self, "port", port) + def with_port(self, port: OutPortV) -> "PortVariable": + """Returns a copy of with variable backed by a different port.""" + return PortVariable(self.name, port, self.defined_at) + CompiledGlobals = dict[DefId, CompiledDef] CompiledLocals = dict[str, PortVariable] diff --git a/guppylang/compiler/expr_compiler.py b/guppylang/compiler/expr_compiler.py index 999ca046..fe78956c 100644 --- a/guppylang/compiler/expr_compiler.py +++ b/guppylang/compiler/expr_compiler.py @@ -78,7 +78,7 @@ def _new_dfcontainer( # Check that the input names are unique assert len({inp.id for inp in inputs}) == len(inputs), "Inputs are not unique" new_locals = { - name.id: PortVariable(name.id, inp.add_out_port(get_type(name)), name, None) + name.id: PortVariable(name.id, inp.add_out_port(get_type(name)), name) for name in inputs } self.dfg = DFContainer(node, self.dfg.locals | new_locals) @@ -111,7 +111,8 @@ def _new_loop( ) # Update the DFG with the outputs from the loop for name in loop_vars: - self.dfg[name.id].port = loop.add_out_port(get_type(name)) + out_port = loop.add_out_port(get_type(name)) + self.dfg[name.id] = self.dfg[name.id].with_port(out_port) @contextmanager def _new_case( @@ -142,7 +143,8 @@ def _if_true(self, cond: ast.expr, inputs: list[ast.Name]) -> Iterator[None]: yield # Update the DFG with the outputs from the Conditional node for name in inputs: - self.dfg[name.id].port = cond_node.add_out_port(get_type(name)) + out_port = cond_node.add_out_port(get_type(name)) + self.dfg[name.id] = self.dfg[name.id].with_port(out_port) def visit_Constant(self, node: ast.Constant) -> OutPortV: if value := python_value_to_hugr(node.value, get_type(node)): @@ -301,7 +303,7 @@ def visit_DesugaredListComp(self, node: DesugaredListComp) -> OutPortV: list_name = with_type(list_ty, with_loc(node, LocalName(id=next(tmp_vars)))) empty_list = self.graph.add_node(DummyOp("MakeList")) self.dfg[list_name.id] = PortVariable( - list_name.id, empty_list.add_out_port(list_ty), node, None + list_name.id, empty_list.add_out_port(list_ty), node ) def compile_generators(elt: ast.expr, gens: list[DesugaredGenerator]) -> None: @@ -311,8 +313,8 @@ def compile_generators(elt: ast.expr, gens: list[DesugaredGenerator]) -> None: list_port, elt_port = self.visit(list_name), self.visit(elt) push = self.graph.add_node( DummyOp("Push"), inputs=[list_port, elt_port] - ) - self.dfg[list_name.id].port = push.add_out_port(list_port.ty) + ).add_out_port(list_port.ty) + self.dfg[list_name.id] = self.dfg[list_name.id].with_port(push) return # Otherwise, compile the first iterator and construct a TailLoop diff --git a/tests/error/comprehension_errors/guarded2.err b/tests/error/comprehension_errors/guarded2.err index 660a7745..6b45ee27 100644 --- a/tests/error/comprehension_errors/guarded2.err +++ b/tests/error/comprehension_errors/guarded2.err @@ -2,6 +2,6 @@ Guppy compilation failed. Error in file $FILE:18 16: @guppy(module) 17: def foo(qs: linst[tuple[bool, qubit]]) -> list[int]: -18: return [42 for b, q in qs if b if q] +18: return [42 for b, q in qs if b if bar(q)] ^ GuppyTypeError: Variable `q` with linear type `qubit` is not used on all control-flow paths of the list comprehension diff --git a/tests/error/comprehension_errors/guarded2.py b/tests/error/comprehension_errors/guarded2.py index 320ea753..0209775e 100644 --- a/tests/error/comprehension_errors/guarded2.py +++ b/tests/error/comprehension_errors/guarded2.py @@ -15,7 +15,7 @@ def bar(q: qubit) -> bool: @guppy(module) def foo(qs: linst[tuple[bool, qubit]]) -> list[int]: - return [42 for b, q in qs if b if q] + return [42 for b, q in qs if b if bar(q)] module.compile() diff --git a/tests/error/comprehension_errors/pattern_override2.err b/tests/error/comprehension_errors/pattern_override2.err index 897f8c70..1c4c1b62 100644 --- a/tests/error/comprehension_errors/pattern_override2.err +++ b/tests/error/comprehension_errors/pattern_override2.err @@ -1,7 +1,7 @@ Guppy compilation failed. Error in file $FILE:13 11: @guppy(module) -12: def foo(qs: linst[qubit], xs: list[int]) -> linst[qubit]: +12: def foo(qs: linst[qubit], xs: list[int]) -> list[int]: 13: return [q for q in qs for q in xs] ^ GuppyError: Variable `q` with linear type `qubit` is not used diff --git a/tests/error/comprehension_errors/pattern_override2.py b/tests/error/comprehension_errors/pattern_override2.py index 4be18840..45ed3cd1 100644 --- a/tests/error/comprehension_errors/pattern_override2.py +++ b/tests/error/comprehension_errors/pattern_override2.py @@ -9,7 +9,7 @@ @guppy(module) -def foo(qs: linst[qubit], xs: list[int]) -> linst[qubit]: +def foo(qs: linst[qubit], xs: list[int]) -> list[int]: return [q for q in qs for q in xs] diff --git a/tests/error/linear_errors/break_unused.py b/tests/error/linear_errors/break_unused.py index 692cd7dc..5acb798c 100644 --- a/tests/error/linear_errors/break_unused.py +++ b/tests/error/linear_errors/break_unused.py @@ -14,7 +14,7 @@ def new_qubit() -> qubit: @guppy.declare(module) -def measure() -> bool: +def measure(q: qubit) -> bool: ... @@ -26,7 +26,7 @@ def foo(i: int) -> bool: if i == 0: break i -= 1 - b ^= measure(q) + b &= measure(q) return b diff --git a/tests/error/linear_errors/continue_unused.py b/tests/error/linear_errors/continue_unused.py index 4f43de23..b3fd52a9 100644 --- a/tests/error/linear_errors/continue_unused.py +++ b/tests/error/linear_errors/continue_unused.py @@ -14,7 +14,7 @@ def new_qubit() -> qubit: @guppy.declare(module) -def measure() -> bool: +def measure(q: qubit) -> bool: ... @@ -26,7 +26,7 @@ def foo(i: int) -> bool: if i % 10 == 0: break i -= 1 - b ^= measure(q) + b &= measure(q) return b