From 2100855515872454c400f517e60d69f895cdea27 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Fri, 20 Dec 2024 11:41:25 +0100 Subject: [PATCH 1/4] Consider True/False constants during CFG construction --- guppylang/cfg/builder.py | 9 +++++++++ tests/integration/test_inout.py | 3 --- tests/integration/test_linear.py | 3 --- tests/integration/test_while.py | 2 -- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 87715c27..940f5ab8 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -368,6 +368,15 @@ def add_branch(node: ast.expr, cfg: CFG, bb: BB, true_bb: BB, false_bb: BB) -> N builder = BranchBuilder(cfg) builder.visit(node, bb, true_bb, false_bb) + def visit_Constant( + self, node: ast.Constant, bb: BB, true_bb: BB, false_bb: BB + ) -> None: + # Branching on `True` or `False` constant should be unconditional + if isinstance(node.value, bool): + self.cfg.link(bb, true_bb if node.value else false_bb) + else: + self.generic_visit(node, bb, true_bb, false_bb) + def visit_BoolOp(self, node: ast.BoolOp, bb: BB, true_bb: BB, false_bb: BB) -> None: # Add short-circuit evaluation of boolean expression. If there are more than 2 # operators, we turn the flat operator list into a right-nested tree to allow diff --git a/tests/integration/test_inout.py b/tests/integration/test_inout.py index ce2ffd80..21716e81 100644 --- a/tests/integration/test_inout.py +++ b/tests/integration/test_inout.py @@ -298,9 +298,6 @@ def test( s.q = qubit() return i += 1 - # Guppy is not yet smart enough to detect that this code is unreachable - s.q = qubit() - return @guppy(module) def main(s: MyStruct @ owned) -> MyStruct: diff --git a/tests/integration/test_linear.py b/tests/integration/test_linear.py index 03a5cdc0..bd650900 100644 --- a/tests/integration/test_linear.py +++ b/tests/integration/test_linear.py @@ -371,9 +371,6 @@ def test(s: MyStruct @owned) -> MyStruct: while True: s.q = qubit() return s - # Guppy is not yet smart enough to detect that this code is unreachable - s.q = qubit() - return s validate(module.compile()) diff --git a/tests/integration/test_while.py b/tests/integration/test_while.py index c351481a..7a08e213 100644 --- a/tests/integration/test_while.py +++ b/tests/integration/test_while.py @@ -6,7 +6,6 @@ def test_infinite_loop(validate): def foo() -> int: while True: pass - return 0 validate(foo) @@ -41,7 +40,6 @@ def foo(i: int) -> int: if i % 2 == 0: continue x = x + i - return x validate(foo) From 9c87a51cda0aef4e5f83f76ec6e0347fc7ebe182 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Fri, 20 Dec 2024 11:45:41 +0100 Subject: [PATCH 2/4] Detect unreachable code --- guppylang/cfg/builder.py | 25 ++++++++++++++++++++-- guppylang/cfg/cfg.py | 12 +++++++++++ tests/error/misc_errors/unreachable_10.err | 8 +++++++ tests/error/misc_errors/unreachable_10.py | 8 +++++++ tests/error/misc_errors/unreachable_11.err | 8 +++++++ tests/error/misc_errors/unreachable_11.py | 10 +++++++++ tests/error/misc_errors/unreachable_6.err | 8 +++++++ tests/error/misc_errors/unreachable_6.py | 8 +++++++ tests/error/misc_errors/unreachable_7.err | 8 +++++++ tests/error/misc_errors/unreachable_7.py | 8 +++++++ tests/error/misc_errors/unreachable_8.err | 8 +++++++ tests/error/misc_errors/unreachable_8.py | 10 +++++++++ tests/error/misc_errors/unreachable_9.err | 8 +++++++ tests/error/misc_errors/unreachable_9.py | 8 +++++++ 14 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/error/misc_errors/unreachable_10.err create mode 100644 tests/error/misc_errors/unreachable_10.py create mode 100644 tests/error/misc_errors/unreachable_11.err create mode 100644 tests/error/misc_errors/unreachable_11.py create mode 100644 tests/error/misc_errors/unreachable_6.err create mode 100644 tests/error/misc_errors/unreachable_6.py create mode 100644 tests/error/misc_errors/unreachable_7.err create mode 100644 tests/error/misc_errors/unreachable_7.py create mode 100644 tests/error/misc_errors/unreachable_8.err create mode 100644 tests/error/misc_errors/unreachable_8.py create mode 100644 tests/error/misc_errors/unreachable_9.err create mode 100644 tests/error/misc_errors/unreachable_9.py diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 940f5ab8..2d0daedd 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -77,13 +77,34 @@ def build(self, nodes: list[ast.stmt], returns_none: bool, globals: Globals) -> nodes, self.cfg.entry_bb, Jumps(self.cfg.exit_bb, None, None) ) + # Compute reachable BBs + reachable = self.cfg.reachable_from(self.cfg.entry_bb) + # If we're still in a basic block after compiling the whole body, we have to add # an implicit void return - if final_bb is not None: + if final_bb is not None and final_bb in reachable: if not returns_none: raise GuppyError(ExpectedError(nodes[-1], "return statement")) self.cfg.link(final_bb, self.cfg.exit_bb) - + reachable.add(self.cfg.exit_bb) + + # Complain about unreachable code + unreachable = set(self.cfg.bbs) - reachable + for bb in unreachable: + if bb.statements: + raise GuppyError(UnreachableError(bb.statements[0])) + if bb.branch_pred: + raise GuppyError(UnreachableError(bb.branch_pred)) + # Empty unreachable BBs are fine, we just prune them + if bb.successors: + # Since there is no branch expression, there can be at most a single + # successor + [succ] = bb.successors + succ.predecessors.remove(bb) + + # If we made it till here, there are only the reachable BBs left. The only + # exception is the exit BB which should never be dropped, even if unreachable. + self.cfg.bbs = list(reachable | {self.cfg.exit_bb}) return self.cfg def visit_stmts(self, nodes: list[ast.stmt], bb: BB, jumps: Jumps) -> BB | None: diff --git a/guppylang/cfg/cfg.py b/guppylang/cfg/cfg.py index ff408fca..f827219d 100644 --- a/guppylang/cfg/cfg.py +++ b/guppylang/cfg/cfg.py @@ -51,6 +51,18 @@ def ancestors(self, *bbs: T) -> Iterator[T]: yield bb queue += bb.predecessors + def reachable_from(self, bb: T) -> set[T]: + """Returns the set of all BBs reachable from some given BB.""" + queue = {bb} + reachable = set() + while queue: + bb = queue.pop() + if bb not in reachable: + reachable.add(bb) + for succ in bb.successors: + queue.add(succ) + return reachable + class CFG(BaseCFG[BB]): """A control-flow graph of unchecked basic blocks.""" diff --git a/tests/error/misc_errors/unreachable_10.err b/tests/error/misc_errors/unreachable_10.err new file mode 100644 index 00000000..cc472745 --- /dev/null +++ b/tests/error/misc_errors/unreachable_10.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:8:4) + | +6 | while True: +7 | x += 1 +8 | return x + | ^^^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_10.py b/tests/error/misc_errors/unreachable_10.py new file mode 100644 index 00000000..9b34f227 --- /dev/null +++ b/tests/error/misc_errors/unreachable_10.py @@ -0,0 +1,8 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + while True: + x += 1 + return x diff --git a/tests/error/misc_errors/unreachable_11.err b/tests/error/misc_errors/unreachable_11.err new file mode 100644 index 00000000..9d933b0a --- /dev/null +++ b/tests/error/misc_errors/unreachable_11.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:9:8) + | +7 | if True: +8 | break +9 | x += 1 + | ^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_11.py b/tests/error/misc_errors/unreachable_11.py new file mode 100644 index 00000000..0e888f26 --- /dev/null +++ b/tests/error/misc_errors/unreachable_11.py @@ -0,0 +1,10 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + while not False: + if True: + break + x += 1 + return x diff --git a/tests/error/misc_errors/unreachable_6.err b/tests/error/misc_errors/unreachable_6.err new file mode 100644 index 00000000..e664b5b1 --- /dev/null +++ b/tests/error/misc_errors/unreachable_6.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:7:8) + | +5 | def foo(x: int) -> int: +6 | if False: +7 | x += 1 + | ^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_6.py b/tests/error/misc_errors/unreachable_6.py new file mode 100644 index 00000000..96a5b969 --- /dev/null +++ b/tests/error/misc_errors/unreachable_6.py @@ -0,0 +1,8 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + if False: + x += 1 + return x diff --git a/tests/error/misc_errors/unreachable_7.err b/tests/error/misc_errors/unreachable_7.err new file mode 100644 index 00000000..12451258 --- /dev/null +++ b/tests/error/misc_errors/unreachable_7.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:8:4) + | +6 | if True: +7 | return 1 +8 | return 0 + | ^^^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_7.py b/tests/error/misc_errors/unreachable_7.py new file mode 100644 index 00000000..2e74ccbe --- /dev/null +++ b/tests/error/misc_errors/unreachable_7.py @@ -0,0 +1,8 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo() -> int: + if True: + return 1 + return 0 diff --git a/tests/error/misc_errors/unreachable_8.err b/tests/error/misc_errors/unreachable_8.err new file mode 100644 index 00000000..bd7e1215 --- /dev/null +++ b/tests/error/misc_errors/unreachable_8.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:9:8) + | +7 | x += 1 +8 | else: +9 | x -= 1 + | ^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_8.py b/tests/error/misc_errors/unreachable_8.py new file mode 100644 index 00000000..7c791619 --- /dev/null +++ b/tests/error/misc_errors/unreachable_8.py @@ -0,0 +1,10 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + if not False: + x += 1 + else: + x -= 1 + return x diff --git a/tests/error/misc_errors/unreachable_9.err b/tests/error/misc_errors/unreachable_9.err new file mode 100644 index 00000000..577064ce --- /dev/null +++ b/tests/error/misc_errors/unreachable_9.err @@ -0,0 +1,8 @@ +Error: Unreachable (at $FILE:6:18) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | if False and (x := x + 1): + | ^^^^^^^^^^ This code is not reachable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_9.py b/tests/error/misc_errors/unreachable_9.py new file mode 100644 index 00000000..6d4ce2b6 --- /dev/null +++ b/tests/error/misc_errors/unreachable_9.py @@ -0,0 +1,8 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + if False and (x := x + 1): + pass + return x From 8a30da1978b17e7abf3af2d1ea12547dd2190c86 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Fri, 20 Dec 2024 11:46:57 +0100 Subject: [PATCH 3/4] Handle unreachable exit --- guppylang/cfg/analysis.py | 12 +++++- guppylang/cfg/cfg.py | 7 +++- guppylang/checker/cfg_checker.py | 15 ++++++- guppylang/checker/linearity_checker.py | 25 ++++++++++-- guppylang/compiler/cfg_compiler.py | 11 ++++++ .../linear_errors/unused_non_terminating.err | 10 +++++ .../linear_errors/unused_non_terminating.py | 18 +++++++++ tests/integration/test_inout.py | 39 +++++++++++++++++++ tests/integration/test_linear.py | 14 +++++++ 9 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 tests/error/linear_errors/unused_non_terminating.err create mode 100644 tests/error/linear_errors/unused_non_terminating.py diff --git a/guppylang/cfg/analysis.py b/guppylang/cfg/analysis.py index cbe3742d..99fbcc7d 100644 --- a/guppylang/cfg/analysis.py +++ b/guppylang/cfg/analysis.py @@ -97,8 +97,13 @@ class LivenessAnalysis(Generic[VId], BackwardAnalysis[LivenessDomain[VId]]): stats: dict[BB, VariableStats[VId]] - def __init__(self, stats: dict[BB, VariableStats[VId]]) -> None: + def __init__( + self, + stats: dict[BB, VariableStats[VId]], + initial: LivenessDomain[VId] | None = None, + ) -> None: self.stats = stats + self._initial = initial or {} def eq(self, live1: LivenessDomain[VId], live2: LivenessDomain[VId]) -> bool: # Only check that both contain the same variables. We don't care about the BB @@ -106,7 +111,7 @@ def eq(self, live1: LivenessDomain[VId], live2: LivenessDomain[VId]) -> bool: return live1.keys() == live2.keys() def initial(self) -> LivenessDomain[VId]: - return {} + return self._initial def join(self, *ts: LivenessDomain[VId]) -> LivenessDomain[VId]: res: LivenessDomain[VId] = {} @@ -183,6 +188,9 @@ def join(self, *ts: AssignmentDomain[VId]) -> AssignmentDomain[VId]: def apply_bb( self, val_before: AssignmentDomain[VId], bb: BB ) -> AssignmentDomain[VId]: + # For unreachable BBs, we can assume that everything is assigned + if not bb.predecessors and bb != bb.containing_cfg.entry_bb: + return self.all_vars, self.all_vars stats = self.stats[bb] def_ass_before, maybe_ass_before = val_before return ( diff --git a/guppylang/cfg/cfg.py b/guppylang/cfg/cfg.py index f827219d..7647e5b0 100644 --- a/guppylang/cfg/cfg.py +++ b/guppylang/cfg/cfg.py @@ -96,7 +96,12 @@ def analyze( stats = {bb: bb.compute_variable_stats() for bb in self.bbs} # Mark all borrowed variables as implicitly used in the exit BB stats[self.exit_bb].used |= {x: InoutReturnSentinel(var=x) for x in inout_vars} - self.live_before = LivenessAnalysis(stats).run(self.bbs) + # This also means borrowed variables are always live, so we can use them as the + # initial value in the liveness analysis. This solves the edge case that + # borrowed variables should be considered live, even if the exit is actually + # unreachable (to avoid linearity violations later). + inout_live = {x: self.exit_bb for x in inout_vars} + self.live_before = LivenessAnalysis(stats, initial=inout_live).run(self.bbs) self.ass_before, self.maybe_ass_before = AssignmentAnalysis( stats, def_ass_before, maybe_ass_before ).run_unpacked(self.bbs) diff --git a/guppylang/checker/cfg_checker.py b/guppylang/checker/cfg_checker.py index 74f1b5ce..f074c454 100644 --- a/guppylang/checker/cfg_checker.py +++ b/guppylang/checker/cfg_checker.py @@ -76,8 +76,8 @@ def check_cfg( """ # First, we need to run program analysis ass_before = {v.name for v in inputs} - inout_vars = [v.name for v in inputs if InputFlags.Inout in v.flags] - cfg.analyze(ass_before, ass_before, inout_vars) + inout_vars = [v for v in inputs if InputFlags.Inout in v.flags] + cfg.analyze(ass_before, ass_before, [v.name for v in inout_vars]) # We start by compiling the entry BB checked_cfg: CheckedCFG[Variable] = CheckedCFG([v.ty for v in inputs], return_ty) @@ -123,6 +123,17 @@ def check_cfg( compiled[bb].predecessors.append(pred) pred.successors[num_output] = compiled[bb] + # The exit BB might be unreachable. In that case it won't be visited above and we + # have to handle it here + if cfg.exit_bb not in compiled: + assert len(cfg.exit_bb.predecessors) == 0 + assert len(cfg.exit_bb.successors) == 0 + assert len(cfg.exit_bb.statements) == 0 + assert cfg.exit_bb.branch_pred is None + compiled[cfg.exit_bb] = CheckedBB( + cfg.exit_bb.idx, checked_cfg, sig=Signature(inout_vars, []) + ) + checked_cfg.bbs = list(compiled.values()) checked_cfg.exit_bb = compiled[cfg.exit_bb] # TODO: Fails if exit is unreachable checked_cfg.live_before = {compiled[bb]: cfg.live_before[bb] for bb in cfg.bbs} diff --git a/guppylang/checker/linearity_checker.py b/guppylang/checker/linearity_checker.py index d2d6d323..72f2983e 100644 --- a/guppylang/checker/linearity_checker.py +++ b/guppylang/checker/linearity_checker.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, NamedTuple, TypeGuard from guppylang.ast_util import AstNode, find_nodes, get_type -from guppylang.cfg.analysis import LivenessAnalysis +from guppylang.cfg.analysis import LivenessAnalysis, LivenessDomain from guppylang.cfg.bb import BB, VariableStats from guppylang.checker.cfg_checker import CheckedBB, CheckedCFG, Row, Signature from guppylang.checker.core import ( @@ -610,9 +610,28 @@ def check_cfg_linearity( for leaf in leaf_places(var): exit_scope.use(leaf.id, InoutReturnSentinel(var=var), UseKind.RETURN) - # Run liveness analysis + # Edge case: If the exit is unreachable, then the function will never terminate, so + # there is no need to give the borrowed values back to the caller. To ensure that + # the generated Hugr is still valid, we have to thread the borrowed arguments + # through the non-terminating loop. We achieve this by considering borrowed + # variables as live in every BB, even if the actual use in the exit is unreachable. + # This is done by including borrowed vars in the initial value for the liveness + # analysis below. The analogous thing was also done in the previous `CFG.analyze` + # pass. + live_default: LivenessDomain[PlaceId] = ( + { + leaf.id: cfg.exit_bb + for var in cfg.entry_bb.sig.input_row + if InputFlags.Inout in var.flags + for leaf in leaf_places(var) + } + if not cfg.exit_bb.predecessors + else {} + ) + + # Run liveness analysis with this initial value stats = {bb: scope.stats() for bb, scope in scopes.items()} - live_before = LivenessAnalysis(stats).run(cfg.bbs) + live_before = LivenessAnalysis(stats, initial=live_default).run(cfg.bbs) # Construct a CFG that tracks places instead of just variables result_cfg: CheckedCFG[Place] = CheckedCFG(cfg.input_tys, cfg.output_ty) diff --git a/guppylang/compiler/cfg_compiler.py b/guppylang/compiler/cfg_compiler.py index ec59c30b..cbb137a3 100644 --- a/guppylang/compiler/cfg_compiler.py +++ b/guppylang/compiler/cfg_compiler.py @@ -41,6 +41,17 @@ def compile_cfg( builder = container.add_cfg(*inputs) + # Explicitly annotate the output types since Hugr can't infer them if the exit is + # unreachable + out_tys = [place.ty.to_hugr() for place in cfg.exit_bb.sig.input_row] + # TODO: Use proper API for this once it's added in hugr-py: + # https://github.com/CQCL/hugr/issues/1816 + builder._exit_op._cfg_outputs = out_tys + builder.parent_op._outputs = out_tys + builder.parent_node = builder.hugr._update_node_outs( + builder.parent_node, len(out_tys) + ) + blocks: dict[CheckedBB[Place], ToNode] = {} for bb in cfg.bbs: blocks[bb] = compile_bb(bb, builder, bb == cfg.entry_bb, globals) diff --git a/tests/error/linear_errors/unused_non_terminating.err b/tests/error/linear_errors/unused_non_terminating.err new file mode 100644 index 00000000..48757e3f --- /dev/null +++ b/tests/error/linear_errors/unused_non_terminating.err @@ -0,0 +1,10 @@ +Error: Linearity violation (at $FILE:13:8) + | +11 | +12 | @guppy(module) +13 | def foo(q: qubit @owned) -> None: + | ^^^^^^^^^^^^^^^ Variable `q` with linear type `qubit` is leaked + +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_non_terminating.py b/tests/error/linear_errors/unused_non_terminating.py new file mode 100644 index 00000000..c4dafdc8 --- /dev/null +++ b/tests/error/linear_errors/unused_non_terminating.py @@ -0,0 +1,18 @@ +import guppylang.std.quantum as quantum +from guppylang.decorator import guppy +from guppylang.module import GuppyModule +from guppylang.std.builtins import owned +from guppylang.std.quantum import qubit + + +module = GuppyModule("test") +module.load_all(quantum) + + +@guppy(module) +def foo(q: qubit @owned) -> None: + while True: + pass + + +module.compile() diff --git a/tests/integration/test_inout.py b/tests/integration/test_inout.py index 21716e81..cb4ebf4c 100644 --- a/tests/integration/test_inout.py +++ b/tests/integration/test_inout.py @@ -373,3 +373,42 @@ def test() -> bool: return result validate(module.compile()) + + +def test_non_terminating(validate): + module = GuppyModule("test") + module.load_all(quantum) + + @guppy.struct(module) + class MyStruct: + q1: qubit + q2: qubit + x: int + + @guppy.declare(module) + def foo(q: qubit) -> None: ... + + @guppy.declare(module) + def bar(s: MyStruct) -> None: ... + + @guppy(module) + def test1(b: bool) -> None: + q = qubit() + s = MyStruct(qubit(), qubit(), 0) + while True: + foo(q) + bar(s) + + @guppy(module) + def test2(q: qubit, s: MyStruct, b: bool) -> None: + while True: + foo(q) + if b: + bar(s) + + @guppy(module) + def test3(q: qubit, s: MyStruct) -> None: + while True: + pass + + validate(module.compile()) diff --git a/tests/integration/test_linear.py b/tests/integration/test_linear.py index bd650900..cf0e6013 100644 --- a/tests/integration/test_linear.py +++ b/tests/integration/test_linear.py @@ -502,3 +502,17 @@ def owned_arg() -> list[qubit]: return qs validate(module.compile()) + + +def test_non_terminating(validate): + module = GuppyModule("test") + module.load_all(quantum_functional) + module.load(qubit) + + @guppy(module) + def test() -> None: + q = qubit() + while True: + q = h(q) + + validate(module.compile()) From 02cfca24336c28980159514696a66c0ff457bced Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Thu, 2 Jan 2025 12:24:15 +0100 Subject: [PATCH 4/4] Allow unreachable code --- guppylang/cfg/analysis.py | 34 ++++- guppylang/cfg/bb.py | 13 +- guppylang/cfg/builder.py | 57 ++++---- guppylang/cfg/cfg.py | 27 ++-- guppylang/checker/cfg_checker.py | 48 ++++--- guppylang/checker/linearity_checker.py | 13 +- guppylang/compiler/cfg_compiler.py | 5 +- .../if_not_defined_unreachable.err | 8 ++ .../if_not_defined_unreachable.py} | 5 +- tests/error/misc_errors/unreachable_1.err | 8 -- tests/error/misc_errors/unreachable_10.err | 8 -- tests/error/misc_errors/unreachable_10.py | 8 -- tests/error/misc_errors/unreachable_11.err | 8 -- tests/error/misc_errors/unreachable_11.py | 10 -- tests/error/misc_errors/unreachable_2.err | 8 -- tests/error/misc_errors/unreachable_2.py | 10 -- tests/error/misc_errors/unreachable_3.err | 8 -- tests/error/misc_errors/unreachable_3.py | 8 -- tests/error/misc_errors/unreachable_4.err | 8 -- tests/error/misc_errors/unreachable_4.py | 8 -- tests/error/misc_errors/unreachable_5.err | 8 -- tests/error/misc_errors/unreachable_5.py | 8 -- tests/error/misc_errors/unreachable_6.err | 8 -- tests/error/misc_errors/unreachable_6.py | 8 -- tests/error/misc_errors/unreachable_7.err | 8 -- tests/error/misc_errors/unreachable_7.py | 8 -- tests/error/misc_errors/unreachable_8.err | 8 -- tests/error/misc_errors/unreachable_8.py | 10 -- tests/error/misc_errors/unreachable_9.err | 8 -- tests/error/misc_errors/unreachable_9.py | 8 -- tests/error/type_errors/unreachable.err | 8 ++ tests/error/type_errors/unreachable.py | 9 ++ tests/integration/test_unreachable.py | 129 ++++++++++++++++++ 33 files changed, 290 insertions(+), 240 deletions(-) create mode 100644 tests/error/errors_on_usage/if_not_defined_unreachable.err rename tests/error/{misc_errors/unreachable_1.py => errors_on_usage/if_not_defined_unreachable.py} (63%) delete mode 100644 tests/error/misc_errors/unreachable_1.err delete mode 100644 tests/error/misc_errors/unreachable_10.err delete mode 100644 tests/error/misc_errors/unreachable_10.py delete mode 100644 tests/error/misc_errors/unreachable_11.err delete mode 100644 tests/error/misc_errors/unreachable_11.py delete mode 100644 tests/error/misc_errors/unreachable_2.err delete mode 100644 tests/error/misc_errors/unreachable_2.py delete mode 100644 tests/error/misc_errors/unreachable_3.err delete mode 100644 tests/error/misc_errors/unreachable_3.py delete mode 100644 tests/error/misc_errors/unreachable_4.err delete mode 100644 tests/error/misc_errors/unreachable_4.py delete mode 100644 tests/error/misc_errors/unreachable_5.err delete mode 100644 tests/error/misc_errors/unreachable_5.py delete mode 100644 tests/error/misc_errors/unreachable_6.err delete mode 100644 tests/error/misc_errors/unreachable_6.py delete mode 100644 tests/error/misc_errors/unreachable_7.err delete mode 100644 tests/error/misc_errors/unreachable_7.py delete mode 100644 tests/error/misc_errors/unreachable_8.err delete mode 100644 tests/error/misc_errors/unreachable_8.py delete mode 100644 tests/error/misc_errors/unreachable_9.err delete mode 100644 tests/error/misc_errors/unreachable_9.py create mode 100644 tests/error/type_errors/unreachable.err create mode 100644 tests/error/type_errors/unreachable.py create mode 100644 tests/integration/test_unreachable.py diff --git a/guppylang/cfg/analysis.py b/guppylang/cfg/analysis.py index 99fbcc7d..d55339ca 100644 --- a/guppylang/cfg/analysis.py +++ b/guppylang/cfg/analysis.py @@ -18,6 +18,11 @@ def eq(self, t1: T, t2: T, /) -> bool: """Equality on lattice values""" return t1 == t2 + @abstractmethod + def include_unreachable(self) -> bool: + """Whether unreachable BBs and jumps should be taken into account for the + analysis.""" + @abstractmethod def initial(self) -> T: """Initial lattice value""" @@ -46,12 +51,19 @@ def run(self, bbs: Iterable[BB]) -> Result[T]: Returns a mapping from basic blocks to lattice values at the start of each BB. """ + if not self.include_unreachable(): + bbs = [bb for bb in bbs if bb.reachable] vals_before = {bb: self.initial() for bb in bbs} # return value vals_after = {bb: self.apply_bb(vals_before[bb], bb) for bb in bbs} # cache queue = set(bbs) while len(queue) > 0: bb = queue.pop() - vals_before[bb] = self.join(*(vals_after[pred] for pred in bb.predecessors)) + preds = ( + bb.predecessors + bb.dummy_predecessors + if self.include_unreachable() + else bb.predecessors + ) + vals_before[bb] = self.join(*(vals_after[pred] for pred in preds)) val_after = self.apply_bb(vals_before[bb], bb) if not self.eq(val_after, vals_after[bb]): vals_after[bb] = val_after @@ -75,7 +87,12 @@ def run(self, bbs: Iterable[BB]) -> Result[T]: queue = set(bbs) while len(queue) > 0: bb = queue.pop() - val_after = self.join(*(vals_before[succ] for succ in bb.successors)) + succs = ( + bb.successors + bb.dummy_successors + if self.include_unreachable() + else bb.successors + ) + val_after = self.join(*(vals_before[succ] for succ in succs)) val_before = self.apply_bb(val_after, bb) if not self.eq(vals_before[bb], val_before): vals_before[bb] = val_before @@ -101,9 +118,11 @@ def __init__( self, stats: dict[BB, VariableStats[VId]], initial: LivenessDomain[VId] | None = None, + include_unreachable: bool = False, ) -> None: self.stats = stats self._initial = initial or {} + self._include_unreachable = include_unreachable def eq(self, live1: LivenessDomain[VId], live2: LivenessDomain[VId]) -> bool: # Only check that both contain the same variables. We don't care about the BB @@ -113,6 +132,9 @@ def eq(self, live1: LivenessDomain[VId], live2: LivenessDomain[VId]) -> bool: def initial(self) -> LivenessDomain[VId]: return self._initial + def include_unreachable(self) -> bool: + return self._include_unreachable + def join(self, *ts: LivenessDomain[VId]) -> LivenessDomain[VId]: res: LivenessDomain[VId] = {} for t in ts: @@ -155,6 +177,7 @@ def __init__( stats: dict[BB, VariableStats[VId]], ass_before_entry: set[VId], maybe_ass_before_entry: set[VId], + include_unreachable: bool = False, ) -> None: """Constructs an `AssignmentAnalysis` pass for a CFG. @@ -169,12 +192,16 @@ def __init__( set.union(*(set(stat.assigned.keys()) for stat in stats.values())) | ass_before_entry ) + self._include_unreachable = include_unreachable def initial(self) -> AssignmentDomain[VId]: # Note that definite assignment must start with `all_vars` instead of only # `ass_before_entry` since we want to compute the *greatest* fixpoint. return self.all_vars, self.maybe_ass_before_entry + def include_unreachable(self) -> bool: + return self._include_unreachable + def join(self, *ts: AssignmentDomain[VId]) -> AssignmentDomain[VId]: # We always include the variables that are definitely assigned before the entry, # even if the join is empty @@ -188,9 +215,6 @@ def join(self, *ts: AssignmentDomain[VId]) -> AssignmentDomain[VId]: def apply_bb( self, val_before: AssignmentDomain[VId], bb: BB ) -> AssignmentDomain[VId]: - # For unreachable BBs, we can assume that everything is assigned - if not bb.predecessors and bb != bb.containing_cfg.entry_bb: - return self.all_vars, self.all_vars stats = self.stats[bb] def_ass_before, maybe_ass_before = val_before return ( diff --git a/guppylang/cfg/bb.py b/guppylang/cfg/bb.py index 776ff333..ce63fe37 100644 --- a/guppylang/cfg/bb.py +++ b/guppylang/cfg/bb.py @@ -63,6 +63,15 @@ class BB(ABC): predecessors: list[Self] = field(default_factory=list) successors: list[Self] = field(default_factory=list) + # Whether this BB is reachable from the entry + reachable: bool = False + + # Dummy predecessors and successors that correspond to branches that are provably + # never taken. For example, `if False: ...` statements emit only dummy control-flow + # links. + dummy_predecessors: list[Self] = field(default_factory=list) + dummy_successors: list[Self] = field(default_factory=list) + # If the BB has multiple successors, we need a predicate to decide to which one to # jump to branch_pred: ast.expr | None = None @@ -93,9 +102,7 @@ def compute_variable_stats(self) -> VariableStats[str]: @property def is_exit(self) -> bool: """Whether this is the exit BB.""" - # The exit BB is the only one without successors (otherwise we would have gotten - # an unreachable code error during CFG building) - return len(self.successors) == 0 + return self == self.containing_cfg.exit_bb class VariableVisitor(ast.NodeVisitor): diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 2d0daedd..094a601c 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -78,41 +78,47 @@ def build(self, nodes: list[ast.stmt], returns_none: bool, globals: Globals) -> ) # Compute reachable BBs - reachable = self.cfg.reachable_from(self.cfg.entry_bb) + self.cfg.update_reachable() # If we're still in a basic block after compiling the whole body, we have to add # an implicit void return - if final_bb is not None and final_bb in reachable: - if not returns_none: - raise GuppyError(ExpectedError(nodes[-1], "return statement")) + if final_bb is not None: self.cfg.link(final_bb, self.cfg.exit_bb) - reachable.add(self.cfg.exit_bb) - - # Complain about unreachable code - unreachable = set(self.cfg.bbs) - reachable - for bb in unreachable: - if bb.statements: - raise GuppyError(UnreachableError(bb.statements[0])) - if bb.branch_pred: - raise GuppyError(UnreachableError(bb.branch_pred)) - # Empty unreachable BBs are fine, we just prune them - if bb.successors: - # Since there is no branch expression, there can be at most a single - # successor - [succ] = bb.successors - succ.predecessors.remove(bb) - - # If we made it till here, there are only the reachable BBs left. The only - # exception is the exit BB which should never be dropped, even if unreachable. - self.cfg.bbs = list(reachable | {self.cfg.exit_bb}) + if final_bb.reachable: + self.cfg.exit_bb.reachable = True + if not returns_none: + raise GuppyError(ExpectedError(nodes[-1], "return statement")) + + # Prune the CFG such that there are no jumps from unreachable code back into + # reachable code. Otherwise, unreachable code could lead to unnecessary type + # checking errors, e.g. if unreachable code changes the type of a variable. + for bb in self.cfg.bbs: + if not bb.reachable: + for succ in list(bb.successors): + if succ.reachable: + bb.successors.remove(succ) + succ.predecessors.remove(bb) + # Similarly, if a BB is reachable, then there is no need to hold on to dummy + # jumps into it. Dummy jumps are only needed to propagate type information + # into and between unreachable BBs + else: + for pred in bb.dummy_predecessors: + pred.dummy_successors.remove(bb) + bb.dummy_predecessors = [] + return self.cfg def visit_stmts(self, nodes: list[ast.stmt], bb: BB, jumps: Jumps) -> BB | None: + prev_bb = bb bb_opt: BB | None = bb next_functional = False for node in nodes: + # If the previous statement jumped, then all following statements are + # unreachable. Just create a new dummy BB and keep going so we can still + # check the unreachable code. if bb_opt is None: - raise GuppyError(UnreachableError(node)) + bb_opt = self.cfg.new_bb() + self.cfg.dummy_link(prev_bb, bb_opt) if is_functional_annotation(node): next_functional = True continue @@ -122,7 +128,7 @@ def visit_stmts(self, nodes: list[ast.stmt], bb: BB, jumps: Jumps) -> BB | None: raise NotImplementedError next_functional = False else: - bb_opt = self.visit(node, bb_opt, jumps) + prev_bb, bb_opt = bb_opt, self.visit(node, bb_opt, jumps) return bb_opt def _build_node_value(self, node: BBStatement, bb: BB) -> BB: @@ -395,6 +401,7 @@ def visit_Constant( # Branching on `True` or `False` constant should be unconditional if isinstance(node.value, bool): self.cfg.link(bb, true_bb if node.value else false_bb) + self.cfg.dummy_link(bb, false_bb if node.value else true_bb) else: self.generic_visit(node, bb, true_bb, false_bb) diff --git a/guppylang/cfg/cfg.py b/guppylang/cfg/cfg.py index 7647e5b0..e1110d33 100644 --- a/guppylang/cfg/cfg.py +++ b/guppylang/cfg/cfg.py @@ -51,17 +51,15 @@ def ancestors(self, *bbs: T) -> Iterator[T]: yield bb queue += bb.predecessors - def reachable_from(self, bb: T) -> set[T]: - """Returns the set of all BBs reachable from some given BB.""" - queue = {bb} - reachable = set() + def update_reachable(self) -> None: + """Sets the reachability flags on the BBs in this CFG.""" + queue = {self.entry_bb} while queue: bb = queue.pop() - if bb not in reachable: - reachable.add(bb) + if not bb.reachable: + bb.reachable = True for succ in bb.successors: queue.add(succ) - return reachable class CFG(BaseCFG[BB]): @@ -87,6 +85,15 @@ def link(self, src_bb: BB, tgt_bb: BB) -> None: src_bb.successors.append(tgt_bb) tgt_bb.predecessors.append(src_bb) + def dummy_link(self, src_bb: BB, tgt_bb: BB) -> None: + """Adds a dummy control-flow edge between two basic blocks that is provably + never taken. + + For example, a `if False: ...` statement emits such a dummy link. + """ + src_bb.dummy_successors.append(tgt_bb) + tgt_bb.dummy_predecessors.append(src_bb) + def analyze( self, def_ass_before: set[str], @@ -101,8 +108,10 @@ def analyze( # borrowed variables should be considered live, even if the exit is actually # unreachable (to avoid linearity violations later). inout_live = {x: self.exit_bb for x in inout_vars} - self.live_before = LivenessAnalysis(stats, initial=inout_live).run(self.bbs) + self.live_before = LivenessAnalysis( + stats, initial=inout_live, include_unreachable=True + ).run(self.bbs) self.ass_before, self.maybe_ass_before = AssignmentAnalysis( - stats, def_ass_before, maybe_ass_before + stats, def_ass_before, maybe_ass_before, include_unreachable=True ).run_unpacked(self.bbs) return stats diff --git a/guppylang/checker/cfg_checker.py b/guppylang/checker/cfg_checker.py index f074c454..d78c3dec 100644 --- a/guppylang/checker/cfg_checker.py +++ b/guppylang/checker/cfg_checker.py @@ -35,9 +35,11 @@ class Signature(Generic[V]): input_row: Row[V] output_rows: Sequence[Row[V]] # One for each successor + dummy_output_rows: Sequence[Row[V]] = field(default_factory=list) + @staticmethod def empty() -> "Signature[V]": - return Signature([], []) + return Signature([], [], []) @dataclass(eq=False) # Disable equality to recover hash from `object` @@ -93,13 +95,16 @@ def check_cfg( (checked_cfg.entry_bb, i, succ) # We enumerate the successor starting from the back, so we start with the `True` # branch. This way, we find errors in a more natural order - for i, succ in reverse_enumerate(cfg.entry_bb.successors) + for i, succ in reverse_enumerate( + cfg.entry_bb.successors + cfg.entry_bb.dummy_successors + ) ) while len(queue) > 0: pred, num_output, bb = queue.popleft() + pred_outputs = [*pred.sig.output_rows, *pred.sig.dummy_output_rows] input_row = [ Variable(v.name, v.ty, v.defined_at, v.flags) - for v in pred.sig.output_rows[num_output] + for v in pred_outputs[num_output] ] if bb in compiled: @@ -119,27 +124,26 @@ def check_cfg( ] compiled[bb] = checked_bb - # Link up BBs in the checked CFG - compiled[bb].predecessors.append(pred) - pred.successors[num_output] = compiled[bb] + # Link up BBs in the checked CFG, excluding the unreachable ones + if bb.reachable: + compiled[bb].predecessors.append(pred) + pred.successors[num_output] = compiled[bb] # The exit BB might be unreachable. In that case it won't be visited above and we # have to handle it here if cfg.exit_bb not in compiled: - assert len(cfg.exit_bb.predecessors) == 0 - assert len(cfg.exit_bb.successors) == 0 - assert len(cfg.exit_bb.statements) == 0 - assert cfg.exit_bb.branch_pred is None + assert not cfg.exit_bb.reachable compiled[cfg.exit_bb] = CheckedBB( - cfg.exit_bb.idx, checked_cfg, sig=Signature(inout_vars, []) + cfg.exit_bb.idx, checked_cfg, reachable=False, sig=Signature(inout_vars, []) ) - checked_cfg.bbs = list(compiled.values()) - checked_cfg.exit_bb = compiled[cfg.exit_bb] # TODO: Fails if exit is unreachable - checked_cfg.live_before = {compiled[bb]: cfg.live_before[bb] for bb in cfg.bbs} - checked_cfg.ass_before = {compiled[bb]: cfg.ass_before[bb] for bb in cfg.bbs} + required_bbs = [bb for bb in cfg.bbs if bb.reachable or bb.is_exit] + checked_cfg.bbs = [compiled[bb] for bb in required_bbs] + checked_cfg.exit_bb = compiled[cfg.exit_bb] + checked_cfg.live_before = {compiled[bb]: cfg.live_before[bb] for bb in required_bbs} + checked_cfg.ass_before = {compiled[bb]: cfg.ass_before[bb] for bb in required_bbs} checked_cfg.maybe_ass_before = { - compiled[bb]: cfg.maybe_ass_before[bb] for bb in cfg.bbs + compiled[bb]: cfg.maybe_ass_before[bb] for bb in required_bbs } # Finally, run the linearity check @@ -216,7 +220,7 @@ def check_bb( bb.branch_pred, ty = ExprSynthesizer(ctx).synthesize(bb.branch_pred) bb.branch_pred, _ = to_bool(bb.branch_pred, ty, ctx) - for succ in bb.successors: + for succ in bb.successors + bb.dummy_successors: for x, use_bb in cfg.live_before[succ].items(): # Check that the variables requested by the successor are defined if x not in ctx.locals and x not in ctx.globals: @@ -238,10 +242,18 @@ def check_bb( [ctx.locals[x] for x in cfg.live_before[succ] if x in ctx.locals] for succ in bb.successors ] + dummy_outputs = [ + [ctx.locals[x] for x in cfg.live_before[succ] if x in ctx.locals] + for succ in bb.dummy_successors + ] # Also prepare the successor list so we can fill it in later checked_bb = CheckedBB( - bb.idx, checked_cfg, checked_stmts, sig=Signature(inputs, outputs) + bb.idx, + checked_cfg, + checked_stmts, + reachable=bb.reachable, + sig=Signature(inputs, outputs, dummy_outputs), ) checked_bb.successors = [None] * len(bb.successors) # type: ignore[list-item] checked_bb.branch_pred = bb.branch_pred diff --git a/guppylang/checker/linearity_checker.py b/guppylang/checker/linearity_checker.py index 72f2983e..0f8fdd81 100644 --- a/guppylang/checker/linearity_checker.py +++ b/guppylang/checker/linearity_checker.py @@ -625,13 +625,15 @@ def check_cfg_linearity( if InputFlags.Inout in var.flags for leaf in leaf_places(var) } - if not cfg.exit_bb.predecessors + if not cfg.exit_bb.reachable else {} ) # Run liveness analysis with this initial value stats = {bb: scope.stats() for bb, scope in scopes.items()} - live_before = LivenessAnalysis(stats, initial=live_default).run(cfg.bbs) + live_before = LivenessAnalysis( + stats, initial=live_default, include_unreachable=False + ).run(cfg.bbs) # Construct a CFG that tracks places instead of just variables result_cfg: CheckedCFG[Place] = CheckedCFG(cfg.input_tys, cfg.output_ty) @@ -719,7 +721,12 @@ def live_places_row(bb: BB, original_row: Row[Variable]) -> Row[Place]: ], ) checked[bb] = CheckedBB( - bb.idx, result_cfg, bb.statements, branch_pred=bb.branch_pred, sig=sig + bb.idx, + result_cfg, + bb.statements, + branch_pred=bb.branch_pred, + reachable=bb.reachable, + sig=sig, ) # Fill in missing fields of the result CFG diff --git a/guppylang/compiler/cfg_compiler.py b/guppylang/compiler/cfg_compiler.py index cbb137a3..0e0a160b 100644 --- a/guppylang/compiler/cfg_compiler.py +++ b/guppylang/compiler/cfg_compiler.py @@ -73,10 +73,13 @@ def compile_bb( If the basic block is the output block, returns `None`. """ # The exit BB is completely empty - if len(bb.successors) == 0: + if bb.is_exit: assert len(bb.statements) == 0 return builder.exit + # Unreachable BBs (besides the exit) should have been removed by now + assert bb.reachable + # Otherwise, we use a regular `Block` node block: hc.Block inputs: Sequence[Place] diff --git a/tests/error/errors_on_usage/if_not_defined_unreachable.err b/tests/error/errors_on_usage/if_not_defined_unreachable.err new file mode 100644 index 00000000..65fb719c --- /dev/null +++ b/tests/error/errors_on_usage/if_not_defined_unreachable.err @@ -0,0 +1,8 @@ +Error: Variable not defined (at $FILE:8:11) + | +6 | if False: +7 | y = 1 +8 | return y + | ^ `y` is not defined + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_1.py b/tests/error/errors_on_usage/if_not_defined_unreachable.py similarity index 63% rename from tests/error/misc_errors/unreachable_1.py rename to tests/error/errors_on_usage/if_not_defined_unreachable.py index 2a2c264b..04b19411 100644 --- a/tests/error/misc_errors/unreachable_1.py +++ b/tests/error/errors_on_usage/if_not_defined_unreachable.py @@ -3,5 +3,6 @@ @compile_guppy def foo() -> int: - return 0 - x = 42 + if False: + y = 1 + return y diff --git a/tests/error/misc_errors/unreachable_1.err b/tests/error/misc_errors/unreachable_1.err deleted file mode 100644 index c2beadd7..00000000 --- a/tests/error/misc_errors/unreachable_1.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:7:4) - | -5 | def foo() -> int: -6 | return 0 -7 | x = 42 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_10.err b/tests/error/misc_errors/unreachable_10.err deleted file mode 100644 index cc472745..00000000 --- a/tests/error/misc_errors/unreachable_10.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:8:4) - | -6 | while True: -7 | x += 1 -8 | return x - | ^^^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_10.py b/tests/error/misc_errors/unreachable_10.py deleted file mode 100644 index 9b34f227..00000000 --- a/tests/error/misc_errors/unreachable_10.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: int) -> int: - while True: - x += 1 - return x diff --git a/tests/error/misc_errors/unreachable_11.err b/tests/error/misc_errors/unreachable_11.err deleted file mode 100644 index 9d933b0a..00000000 --- a/tests/error/misc_errors/unreachable_11.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:9:8) - | -7 | if True: -8 | break -9 | x += 1 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_11.py b/tests/error/misc_errors/unreachable_11.py deleted file mode 100644 index 0e888f26..00000000 --- a/tests/error/misc_errors/unreachable_11.py +++ /dev/null @@ -1,10 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: int) -> int: - while not False: - if True: - break - x += 1 - return x diff --git a/tests/error/misc_errors/unreachable_2.err b/tests/error/misc_errors/unreachable_2.err deleted file mode 100644 index 77ba347f..00000000 --- a/tests/error/misc_errors/unreachable_2.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:10:4) - | - 8 | else: - 9 | return 1 -10 | x = 42 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_2.py b/tests/error/misc_errors/unreachable_2.py deleted file mode 100644 index 6a4a2750..00000000 --- a/tests/error/misc_errors/unreachable_2.py +++ /dev/null @@ -1,10 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: bool) -> int: - if x: - return 4 - else: - return 1 - x = 42 diff --git a/tests/error/misc_errors/unreachable_3.err b/tests/error/misc_errors/unreachable_3.err deleted file mode 100644 index ea97ab28..00000000 --- a/tests/error/misc_errors/unreachable_3.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:8:8) - | -6 | while x: -7 | break -8 | x = 42 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_3.py b/tests/error/misc_errors/unreachable_3.py deleted file mode 100644 index f14290eb..00000000 --- a/tests/error/misc_errors/unreachable_3.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: bool) -> int: - while x: - break - x = 42 diff --git a/tests/error/misc_errors/unreachable_4.err b/tests/error/misc_errors/unreachable_4.err deleted file mode 100644 index 7c96a925..00000000 --- a/tests/error/misc_errors/unreachable_4.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:8:8) - | -6 | while x: -7 | continue -8 | x = 42 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_4.py b/tests/error/misc_errors/unreachable_4.py deleted file mode 100644 index 10b9fac3..00000000 --- a/tests/error/misc_errors/unreachable_4.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: bool) -> int: - while x: - continue - x = 42 diff --git a/tests/error/misc_errors/unreachable_5.err b/tests/error/misc_errors/unreachable_5.err deleted file mode 100644 index 3a34b1e4..00000000 --- a/tests/error/misc_errors/unreachable_5.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:8:8) - | -6 | while x: -7 | return 42 -8 | x = 42 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_5.py b/tests/error/misc_errors/unreachable_5.py deleted file mode 100644 index 7875e472..00000000 --- a/tests/error/misc_errors/unreachable_5.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: bool) -> int: - while x: - return 42 - x = 42 diff --git a/tests/error/misc_errors/unreachable_6.err b/tests/error/misc_errors/unreachable_6.err deleted file mode 100644 index e664b5b1..00000000 --- a/tests/error/misc_errors/unreachable_6.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:7:8) - | -5 | def foo(x: int) -> int: -6 | if False: -7 | x += 1 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_6.py b/tests/error/misc_errors/unreachable_6.py deleted file mode 100644 index 96a5b969..00000000 --- a/tests/error/misc_errors/unreachable_6.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: int) -> int: - if False: - x += 1 - return x diff --git a/tests/error/misc_errors/unreachable_7.err b/tests/error/misc_errors/unreachable_7.err deleted file mode 100644 index 12451258..00000000 --- a/tests/error/misc_errors/unreachable_7.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:8:4) - | -6 | if True: -7 | return 1 -8 | return 0 - | ^^^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_7.py b/tests/error/misc_errors/unreachable_7.py deleted file mode 100644 index 2e74ccbe..00000000 --- a/tests/error/misc_errors/unreachable_7.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo() -> int: - if True: - return 1 - return 0 diff --git a/tests/error/misc_errors/unreachable_8.err b/tests/error/misc_errors/unreachable_8.err deleted file mode 100644 index bd7e1215..00000000 --- a/tests/error/misc_errors/unreachable_8.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:9:8) - | -7 | x += 1 -8 | else: -9 | x -= 1 - | ^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_8.py b/tests/error/misc_errors/unreachable_8.py deleted file mode 100644 index 7c791619..00000000 --- a/tests/error/misc_errors/unreachable_8.py +++ /dev/null @@ -1,10 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: int) -> int: - if not False: - x += 1 - else: - x -= 1 - return x diff --git a/tests/error/misc_errors/unreachable_9.err b/tests/error/misc_errors/unreachable_9.err deleted file mode 100644 index 577064ce..00000000 --- a/tests/error/misc_errors/unreachable_9.err +++ /dev/null @@ -1,8 +0,0 @@ -Error: Unreachable (at $FILE:6:18) - | -4 | @compile_guppy -5 | def foo(x: int) -> int: -6 | if False and (x := x + 1): - | ^^^^^^^^^^ This code is not reachable - -Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_9.py b/tests/error/misc_errors/unreachable_9.py deleted file mode 100644 index 6d4ce2b6..00000000 --- a/tests/error/misc_errors/unreachable_9.py +++ /dev/null @@ -1,8 +0,0 @@ -from tests.util import compile_guppy - - -@compile_guppy -def foo(x: int) -> int: - if False and (x := x + 1): - pass - return x diff --git a/tests/error/type_errors/unreachable.err b/tests/error/type_errors/unreachable.err new file mode 100644 index 00000000..3f53e9c9 --- /dev/null +++ b/tests/error/type_errors/unreachable.err @@ -0,0 +1,8 @@ +Error: Type mismatch (at $FILE:8:15) + | +6 | if False: +7 | # This code is unreachable, but we still type-check it +8 | return 1.0 + | ^^^ Expected return value of type `int`, got `float` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/unreachable.py b/tests/error/type_errors/unreachable.py new file mode 100644 index 00000000..2eaa49d1 --- /dev/null +++ b/tests/error/type_errors/unreachable.py @@ -0,0 +1,9 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo(x: int) -> int: + if False: + # This code is unreachable, but we still type-check it + return 1.0 + return 0 diff --git a/tests/integration/test_unreachable.py b/tests/integration/test_unreachable.py new file mode 100644 index 00000000..fbee5b85 --- /dev/null +++ b/tests/integration/test_unreachable.py @@ -0,0 +1,129 @@ +from guppylang import GuppyModule, guppy, qubit +from guppylang.std import quantum +from guppylang.std.quantum import discard +from tests.util import compile_guppy + + +def test_var_defined1(validate): + @compile_guppy + def test() -> int: + if True: + x = 1 + return x + + validate(test) + + +def test_var_defined2(validate): + @compile_guppy + def test(b: bool) -> int: + while True: + if b: + x = 1 + break + return x + + validate(test) + + +def test_type_mismatch1(validate): + @compile_guppy + def test() -> int: + if True: + x = 1 + else: + x = 1.0 + return x + + validate(test) + + +def test_type_mismatch2(validate): + @compile_guppy + def test() -> int: + x = 1 + while False: + x = 1.0 + return x + + validate(test) + + +def test_type_mismatch3(validate): + @compile_guppy + def test() -> int: + x = 1 + if False and (x := 1.0): + pass + return x + + validate(test) + + +def test_unused_var_use1(validate): + @compile_guppy + def test() -> int: + x = 1 + if True: + return 0 + return x + + validate(test) + + +def test_unused_var_use2(validate): + @compile_guppy + def test() -> int: + x = 1 + if not False: + x = 1.0 + return 0 + return x + + validate(test) + + +def test_unreachable_leak(validate): + module = GuppyModule("module") + module.load_all(quantum) + + @guppy(module) + def test(b: bool) -> int: + q = qubit() + while True: + if b: + discard(q) + return 1 + # This return would leak, but we don't complain since it's unreachable: + return 0 + + validate(module.compile()) + + +def test_unreachable_leak2(validate): + module = GuppyModule("module") + module.load_all(quantum) + + @guppy(module) + def test() -> None: + if False: + # This would leak, but we don't complain since it's unreachable: + q = qubit() + + validate(module.compile()) + + +def test_unreachable_copy(validate): + module = GuppyModule("module") + module.load_all(quantum) + + @guppy(module) + def test() -> None: + q = qubit() + discard(q) + if False: + # This would be a linearity violation, but we don't complain since it's + # unreachable: + h(q) + + validate(module.compile())