From 6d40d4207f8db50c6168337090e77f79d9c92414 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli Date: Thu, 12 Jun 2025 09:36:54 +0530 Subject: [PATCH 01/22] Add unroll_loops support to while loop handling --- src/pyqasm/exceptions.py | 5 ++ src/pyqasm/visitor.py | 54 +++++++++++- tests/test.py | 180 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 tests/test.py diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 7a465c09..7f351576 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -48,6 +48,11 @@ class QasmParsingError(QASM3ParsingError): """An error raised by the AST visitor during the AST-generation phase. This is raised in cases where the given program could not be correctly parsed.""" +class LoopLimitExceededError(PyQasmError): + """Exception raised when a loop limit is exceeded during unrolling or other operations.""" + + def __init__(self, message: str = "Loop limit exceeded."): + super().__init__(message) def raise_qasm3_error( message: Optional[str] = None, diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 0fd6d1ec..b651bf0c 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -30,7 +30,7 @@ from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable -from pyqasm.exceptions import ValidationError, raise_qasm3_error +from pyqasm.exceptions import ValidationError, raise_qasm3_error, LoopLimitExceededError from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS @@ -47,6 +47,11 @@ logger = logging.getLogger(__name__) logger.propagate = False +class LoopControlSignal(Exception): + def __init__(self, signal_type: str): + assert signal_type in ("break", "continue") + self.signal_type = signal_type + # pylint: disable-next=too-many-instance-attributes class QasmVisitor: @@ -68,6 +73,7 @@ def __init__( check_only: bool = False, external_gates: list[str] | None = None, unroll_barriers: bool = True, + loop_limit: int = 1024, ): self._module = module self._scope: deque = deque([{}]) @@ -93,6 +99,7 @@ def __init__( self._is_branch_qubits: set[tuple[str, int]] = set() self._is_branch_clbits: set[tuple[str, int]] = set() self._init_utilities() + self._loop_limit = loop_limit def _init_utilities(self): """Initialize the utilities for the visitor.""" @@ -877,6 +884,12 @@ def _visit_basic_gate_operation( return [] return result + + def _visit_break(self, statement: qasm3_ast.BreakStatement) -> list[qasm3_ast.Statement]: + raise LoopControlSignal("break") + + def _visit_continue(self, statement: qasm3_ast.ContinueStatement) -> list[qasm3_ast.Statement]: + raise LoopControlSignal("continue") def _visit_custom_gate_operation( self, @@ -1990,8 +2003,40 @@ def _visit_function_call( return return_value, result - def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: - pass + def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.Statement]: + result = [] + + loop_counter = 0 + max_iterations = self._loop_limit + + while True: + cond_value = Qasm3ExprEvaluator.evaluate_expression(statement.while_condition)[0] + if not cond_value: + break + + self._push_context(Context.BLOCK) + self._push_scope({}) + + try: + result.extend(self.visit_basic_block(statement.block)) + except LoopControlSignal as lcs: + self._pop_scope() + self._restore_context() + if lcs.signal_type == "break": + break + elif lcs.signal_type == "continue": + continue + + self._pop_scope() + self._restore_context() + + loop_counter += 1 + if loop_counter >= max_iterations: + raise LoopLimitExceededError("Loop exceeded max allowed iterations") + + return result + + def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[None]: """Visit an alias statement element. @@ -2232,11 +2277,14 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat qasm3_ast.ConstantDeclaration: self._visit_constant_declaration, qasm3_ast.BranchingStatement: self._visit_branching_statement, qasm3_ast.ForInLoop: self._visit_forin_loop, + qasm3_ast.WhileLoop: self._visit_while_loop, qasm3_ast.AliasStatement: self._visit_alias_statement, qasm3_ast.SwitchStatement: self._visit_switch_statement, qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), qasm3_ast.IODeclaration: lambda x: [], + qasm3_ast.BreakStatement: lambda x: self._visit_break(x), + qasm3_ast.ContinueStatement: lambda x: self._visit_continue(x), } visitor_function = visit_map.get(type(statement)) diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 00000000..c1e0c317 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,180 @@ +import pytest + +from pyqasm import loads +from pyqasm.exceptions import LoopLimitExceededError +from tests.utils import ( + check_single_qubit_gate_op +) + +def test_while_loop_with_continue(): + """Test a while loop with break and continue statements.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + bit[3] c; + int i = 0; + while (i < 3) { + if (i == 1) { + i += 1; + continue; + } + h q[i]; + i += 1; + } + measure q -> c; + + """ + + result = loads(qasm_str) + result.unroll() + + + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 2], "h") + +def test_while_loop_with_break(): + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + bit[3] c; + int i = 0; + while (i < 3) { + if (i == 1) { + break; + } + h q[i]; + i += 1; + } + measure q -> c; + """ + + result = loads(qasm_str) + result.unroll() + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_while_loop_unroll_qasm_output(): + """Test that unrolling a while loop produces the expected QASM string.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 2) { + h q; + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + # Validate number of h q operations + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + + +def test_empty_while_loop_ignored(): + """Test that an empty while loop is ignored (no effect).""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 0) { + } + h q; + """ + result = loads(qasm_str) + result.unroll() + # Only one h q operation should be present + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_nested_while_loops_break_continue(): + """Test nested while loops: break/continue in inner loop does not affect outer loop.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + int j = 0; + while (i < 2) { + j = 0; + while (j < 2) { + if (j == 1) { + break; + } + j += 1; + } + i += 1; + } + h q; + """ + result = loads(qasm_str) + result.unroll() + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_mixed_for_while_loops(): + """Test a for loop inside a while loop and vice versa.""" + qasm_str = """ + OPENQASM 3.0; + qubit[2] q; + int i = 0; + while (i < 2) { + for int j in {0, 1} { + h q[j]; + } + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + # Validate number of h operations and indices + check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 0, 1], "h") + + +def test_while_loop_scope(): + """Test that while loop properly handles variable scoping.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + int j = 0; + while (i < 2) { + int k = i; + h q; + j += k; + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + + +def test_while_loop_quantum_measurement(): + """Test that while loop with quantum measurement in condition raises error.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c) { + h q; + c = measure q; + } + """ + result = loads(qasm_str) + result.unroll() + +def test_while_loop_limit_exceeded(): + """Test that exceeding the loop limit raises LoopLimitExceeded.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 10) { + i += 1; + } + """ + result = loads(qasm_str) + with pytest.raises(LoopLimitExceededError): + result.unroll(loop_limit=5) \ No newline at end of file From 1d86010f2247d587e0bbc01c3f4cc8a46ad94263 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:15:35 +0530 Subject: [PATCH 02/22] Update test.py --- tests/test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index c1e0c317..d9cb780e 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,3 +1,6 @@ + +"""Test module for while.""" + import pytest from pyqasm import loads @@ -177,4 +180,5 @@ def test_while_loop_limit_exceeded(): """ result = loads(qasm_str) with pytest.raises(LoopLimitExceededError): - result.unroll(loop_limit=5) \ No newline at end of file + result.unroll(loop_limit=5) + From 83bde5f88dd6b13f516e9712acd5a3fe8c646234 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:20:04 +0530 Subject: [PATCH 03/22] Update visitor.py --- src/pyqasm/visitor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index b651bf0c..b1d994bc 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -73,7 +73,6 @@ def __init__( check_only: bool = False, external_gates: list[str] | None = None, unroll_barriers: bool = True, - loop_limit: int = 1024, ): self._module = module self._scope: deque = deque([{}]) @@ -99,7 +98,7 @@ def __init__( self._is_branch_qubits: set[tuple[str, int]] = set() self._is_branch_clbits: set[tuple[str, int]] = set() self._init_utilities() - self._loop_limit = loop_limit + self._loop_limit = 1024 def _init_utilities(self): """Initialize the utilities for the visitor.""" @@ -884,7 +883,7 @@ def _visit_basic_gate_operation( return [] return result - + def _visit_break(self, statement: qasm3_ast.BreakStatement) -> list[qasm3_ast.Statement]: raise LoopControlSignal("break") @@ -2024,7 +2023,7 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St self._restore_context() if lcs.signal_type == "break": break - elif lcs.signal_type == "continue": + if lcs.signal_type == "continue": continue self._pop_scope() @@ -2036,8 +2035,6 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St return result - - def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[None]: """Visit an alias statement element. @@ -2283,7 +2280,7 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), qasm3_ast.IODeclaration: lambda x: [], - qasm3_ast.BreakStatement: lambda x: self._visit_break(x), + qasm3_ast.BreakStatement: x: self._visit_break(x), qasm3_ast.ContinueStatement: lambda x: self._visit_continue(x), } From d5dae9cfbaaf55f8e22e4bb6ff83e95468ccf123 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:28:19 +0530 Subject: [PATCH 04/22] Update visitor.py --- src/pyqasm/visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index b1d994bc..1f85cc67 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2280,7 +2280,7 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), qasm3_ast.IODeclaration: lambda x: [], - qasm3_ast.BreakStatement: x: self._visit_break(x), + qasm3_ast.BreakStatement: lambda x: self._visit_break(x), qasm3_ast.ContinueStatement: lambda x: self._visit_continue(x), } From 52c99dd26895b4d6db3849aeae5081e2eea55d19 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:30:51 +0530 Subject: [PATCH 05/22] Update visitor.py --- src/pyqasm/visitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 1f85cc67..ee2f4db9 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2280,8 +2280,8 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), qasm3_ast.IODeclaration: lambda x: [], - qasm3_ast.BreakStatement: lambda x: self._visit_break(x), - qasm3_ast.ContinueStatement: lambda x: self._visit_continue(x), + qasm3_ast.BreakStatement: self._visit_break, + qasm3_ast.ContinueStatement: self._visit_continue, } visitor_function = visit_map.get(type(statement)) From 7fe2fdb1880d82f074347cbb4bec02c29d1f71a3 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:03:06 +0530 Subject: [PATCH 06/22] Update test.py --- tests/test.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/tests/test.py b/tests/test.py index d9cb780e..f6c9e422 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,10 +1,26 @@ - -"""Test module for while.""" +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +Module containing unit tests for while loops in OpenQASM 3.0. + +""" import pytest - from pyqasm import loads -from pyqasm.exceptions import LoopLimitExceededError +from pyqasm.exceptions import LoopLimitExceededError, ValidationError from tests.utils import ( check_single_qubit_gate_op ) @@ -174,11 +190,56 @@ def test_while_loop_limit_exceeded(): OPENQASM 3.0; qubit q; int i = 0; - while (i < 10) { + while (i < 1e10) { i += 1; } """ result = loads(qasm_str) with pytest.raises(LoopLimitExceededError): - result.unroll(loop_limit=5) - + result.unroll() + +def test_while_loop_quantum_measurement(): + """Test that while loop with quantum measurement in condition raises error.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c) { + h q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="Cannot unroll while-loop with condition depending on quantum measurement result."): + result = loads(qasm_str) + result.unroll() + +def test_while_loop_measurement_complex_condition(): + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (!(!c)) { + x q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="quantum measurement"): + result = loads(qasm_str) + result.unroll() + +def test_while_loop_measurement_binary_expr(): + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c == 1) { + h q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="quantum measurement"): + result = loads(qasm_str) + result.unroll() From 4fb0a35c8ce96b48192fd9da6da160dc1b7a5149 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:04:37 +0530 Subject: [PATCH 07/22] Create test_while.py --- tests/qasm3/test_while.py | 245 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 tests/qasm3/test_while.py diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py new file mode 100644 index 00000000..f6c9e422 --- /dev/null +++ b/tests/qasm3/test_while.py @@ -0,0 +1,245 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +Module containing unit tests for while loops in OpenQASM 3.0. + +""" + +import pytest +from pyqasm import loads +from pyqasm.exceptions import LoopLimitExceededError, ValidationError +from tests.utils import ( + check_single_qubit_gate_op +) + +def test_while_loop_with_continue(): + """Test a while loop with break and continue statements.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + bit[3] c; + int i = 0; + while (i < 3) { + if (i == 1) { + i += 1; + continue; + } + h q[i]; + i += 1; + } + measure q -> c; + + """ + + result = loads(qasm_str) + result.unroll() + + + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 2], "h") + +def test_while_loop_with_break(): + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + bit[3] c; + int i = 0; + while (i < 3) { + if (i == 1) { + break; + } + h q[i]; + i += 1; + } + measure q -> c; + """ + + result = loads(qasm_str) + result.unroll() + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_while_loop_unroll_qasm_output(): + """Test that unrolling a while loop produces the expected QASM string.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 2) { + h q; + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + # Validate number of h q operations + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + + +def test_empty_while_loop_ignored(): + """Test that an empty while loop is ignored (no effect).""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 0) { + } + h q; + """ + result = loads(qasm_str) + result.unroll() + # Only one h q operation should be present + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_nested_while_loops_break_continue(): + """Test nested while loops: break/continue in inner loop does not affect outer loop.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + int j = 0; + while (i < 2) { + j = 0; + while (j < 2) { + if (j == 1) { + break; + } + j += 1; + } + i += 1; + } + h q; + """ + result = loads(qasm_str) + result.unroll() + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_mixed_for_while_loops(): + """Test a for loop inside a while loop and vice versa.""" + qasm_str = """ + OPENQASM 3.0; + qubit[2] q; + int i = 0; + while (i < 2) { + for int j in {0, 1} { + h q[j]; + } + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + # Validate number of h operations and indices + check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 0, 1], "h") + + +def test_while_loop_scope(): + """Test that while loop properly handles variable scoping.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + int j = 0; + while (i < 2) { + int k = i; + h q; + j += k; + i += 1; + } + """ + result = loads(qasm_str) + result.unroll() + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + + +def test_while_loop_quantum_measurement(): + """Test that while loop with quantum measurement in condition raises error.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c) { + h q; + c = measure q; + } + """ + result = loads(qasm_str) + result.unroll() + +def test_while_loop_limit_exceeded(): + """Test that exceeding the loop limit raises LoopLimitExceeded.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + int i = 0; + while (i < 1e10) { + i += 1; + } + """ + result = loads(qasm_str) + with pytest.raises(LoopLimitExceededError): + result.unroll() + +def test_while_loop_quantum_measurement(): + """Test that while loop with quantum measurement in condition raises error.""" + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c) { + h q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="Cannot unroll while-loop with condition depending on quantum measurement result."): + result = loads(qasm_str) + result.unroll() + +def test_while_loop_measurement_complex_condition(): + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (!(!c)) { + x q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="quantum measurement"): + result = loads(qasm_str) + result.unroll() + +def test_while_loop_measurement_binary_expr(): + qasm_str = """ + OPENQASM 3.0; + qubit q; + bit c; + c = measure q; + while (c == 1) { + h q; + c = measure q; + } + """ + with pytest.raises(ValidationError, match="quantum measurement"): + result = loads(qasm_str) + result.unroll() From 098d1758c226eebb2a54243cd404c10cb4fc61ea Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:05:06 +0530 Subject: [PATCH 08/22] Delete tests/test.py --- tests/test.py | 245 -------------------------------------------------- 1 file changed, 245 deletions(-) delete mode 100644 tests/test.py diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index f6c9e422..00000000 --- a/tests/test.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright 2025 qBraid -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -""" -Module containing unit tests for while loops in OpenQASM 3.0. - -""" - -import pytest -from pyqasm import loads -from pyqasm.exceptions import LoopLimitExceededError, ValidationError -from tests.utils import ( - check_single_qubit_gate_op -) - -def test_while_loop_with_continue(): - """Test a while loop with break and continue statements.""" - qasm_str = """ - OPENQASM 3.0; - include "stdgates.inc"; - qubit[3] q; - bit[3] c; - int i = 0; - while (i < 3) { - if (i == 1) { - i += 1; - continue; - } - h q[i]; - i += 1; - } - measure q -> c; - - """ - - result = loads(qasm_str) - result.unroll() - - - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 2], "h") - -def test_while_loop_with_break(): - qasm_str = """ - OPENQASM 3.0; - include "stdgates.inc"; - qubit[3] q; - bit[3] c; - int i = 0; - while (i < 3) { - if (i == 1) { - break; - } - h q[i]; - i += 1; - } - measure q -> c; - """ - - result = loads(qasm_str) - result.unroll() - - check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") - - -def test_while_loop_unroll_qasm_output(): - """Test that unrolling a while loop produces the expected QASM string.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - int i = 0; - while (i < 2) { - h q; - i += 1; - } - """ - result = loads(qasm_str) - result.unroll() - # Validate number of h q operations - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") - - -def test_empty_while_loop_ignored(): - """Test that an empty while loop is ignored (no effect).""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - int i = 0; - while (i < 0) { - } - h q; - """ - result = loads(qasm_str) - result.unroll() - # Only one h q operation should be present - check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") - - -def test_nested_while_loops_break_continue(): - """Test nested while loops: break/continue in inner loop does not affect outer loop.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - int i = 0; - int j = 0; - while (i < 2) { - j = 0; - while (j < 2) { - if (j == 1) { - break; - } - j += 1; - } - i += 1; - } - h q; - """ - result = loads(qasm_str) - result.unroll() - check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") - - -def test_mixed_for_while_loops(): - """Test a for loop inside a while loop and vice versa.""" - qasm_str = """ - OPENQASM 3.0; - qubit[2] q; - int i = 0; - while (i < 2) { - for int j in {0, 1} { - h q[j]; - } - i += 1; - } - """ - result = loads(qasm_str) - result.unroll() - # Validate number of h operations and indices - check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 0, 1], "h") - - -def test_while_loop_scope(): - """Test that while loop properly handles variable scoping.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - int i = 0; - int j = 0; - while (i < 2) { - int k = i; - h q; - j += k; - i += 1; - } - """ - result = loads(qasm_str) - result.unroll() - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") - - -def test_while_loop_quantum_measurement(): - """Test that while loop with quantum measurement in condition raises error.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - bit c; - c = measure q; - while (c) { - h q; - c = measure q; - } - """ - result = loads(qasm_str) - result.unroll() - -def test_while_loop_limit_exceeded(): - """Test that exceeding the loop limit raises LoopLimitExceeded.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - int i = 0; - while (i < 1e10) { - i += 1; - } - """ - result = loads(qasm_str) - with pytest.raises(LoopLimitExceededError): - result.unroll() - -def test_while_loop_quantum_measurement(): - """Test that while loop with quantum measurement in condition raises error.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - bit c; - c = measure q; - while (c) { - h q; - c = measure q; - } - """ - with pytest.raises(ValidationError, match="Cannot unroll while-loop with condition depending on quantum measurement result."): - result = loads(qasm_str) - result.unroll() - -def test_while_loop_measurement_complex_condition(): - qasm_str = """ - OPENQASM 3.0; - qubit q; - bit c; - c = measure q; - while (!(!c)) { - x q; - c = measure q; - } - """ - with pytest.raises(ValidationError, match="quantum measurement"): - result = loads(qasm_str) - result.unroll() - -def test_while_loop_measurement_binary_expr(): - qasm_str = """ - OPENQASM 3.0; - qubit q; - bit c; - c = measure q; - while (c == 1) { - h q; - c = measure q; - } - """ - with pytest.raises(ValidationError, match="quantum measurement"): - result = loads(qasm_str) - result.unroll() From ed78c70c42e3593a757900989980a431ef3e0f79 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:05:35 +0530 Subject: [PATCH 09/22] Update visitor.py --- src/pyqasm/visitor.py | 78 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index ee2f4db9..d36bab35 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -30,7 +30,7 @@ from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable -from pyqasm.exceptions import ValidationError, raise_qasm3_error, LoopLimitExceededError +from pyqasm.exceptions import ValidationError, raise_qasm3_error, LoopLimitExceededError, LoopControlSignal, BreakSignal, ContinueSignal from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS @@ -47,11 +47,6 @@ logger = logging.getLogger(__name__) logger.propagate = False -class LoopControlSignal(Exception): - def __init__(self, signal_type: str): - assert signal_type in ("break", "continue") - self.signal_type = signal_type - # pylint: disable-next=too-many-instance-attributes class QasmVisitor: @@ -97,8 +92,9 @@ def __init__( self._in_branching_statement: int = 0 self._is_branch_qubits: set[tuple[str, int]] = set() self._is_branch_clbits: set[tuple[str, int]] = set() + self._measurement_set = set() self._init_utilities() - self._loop_limit = 1024 + self._loop_limit = 1e9 def _init_utilities(self): """Initialize the utilities for the visitor.""" @@ -561,6 +557,11 @@ def _visit_measurement( # pylint: disable=too-many-locals qubit_node.depth = max(qubit_node.depth, clbit_node.depth) clbit_node.depth = max(qubit_node.depth, clbit_node.depth) + if isinstance(target, qasm3_ast.Identifier): + self._measurement_set.add(target.name) + elif isinstance(target, qasm3_ast.IndexedIdentifier): + self._measurement_set.add(target.name.name) + unrolled_measurements.append(unrolled_measure) if self._check_only: @@ -883,12 +884,16 @@ def _visit_basic_gate_operation( return [] return result - + def _visit_break(self, statement: qasm3_ast.BreakStatement) -> list[qasm3_ast.Statement]: - raise LoopControlSignal("break") + raise_qasm3_error( + err_type=BreakSignal, + ) def _visit_continue(self, statement: qasm3_ast.ContinueStatement) -> list[qasm3_ast.Statement]: - raise LoopControlSignal("continue") + raise_qasm3_error( + err_type=ContinueSignal, + ) def _visit_custom_gate_operation( self, @@ -1622,7 +1627,7 @@ def _visit_classical_assignment( return [] return statements - + def _evaluate_array_initialization( self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: Any ) -> np.ndarray: @@ -2001,12 +2006,54 @@ def _visit_function_call( return return_value, [] return return_value, result + + def _condition_depends_on_measurement(self, condition: qasm3_ast.Expression) -> bool: + """Recursively check whether the given condition depends on a classical register set by measurement.""" + + def depends(expr: qasm3_ast.Expression) -> bool: + if isinstance(expr, qasm3_ast.Identifier): + return expr.name in self._measurement_set + + elif isinstance(expr, qasm3_ast.IndexExpression): + # Check if the collection being indexed is in the measurement set + if isinstance(expr.collection, qasm3_ast.Identifier): + return expr.collection.name in self._measurement_set + return depends(expr.collection) or depends(expr.index) + + elif isinstance(expr, qasm3_ast.BinaryExpression): + return depends(expr.lhs) or depends(expr.rhs) + + elif isinstance(expr, qasm3_ast.UnaryExpression): + return depends(expr.expression) + + elif isinstance(expr, qasm3_ast.FunctionCall): + return any(depends(arg) for arg in expr.arguments) + return False + + return depends(condition) + def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.Statement]: + + """ Input: + statement (qasm3_ast.WhileLoop) - the while-loop AST node + Output: + list[qasm3_ast.Statement] - flattened/unrolled statements + Raises: + ValidationError - if loop condition is non-classical or dynamic """ + result = [] loop_counter = 0 - max_iterations = self._loop_limit + max_iterations = 5 + + if self._condition_depends_on_measurement(statement.while_condition): + raise_qasm3_error( + "Cannot unroll while-loop with condition depending on quantum measurement result.", + error_node=statement, + span=statement.span, + ) + while True: cond_value = Qasm3ExprEvaluator.evaluate_expression(statement.while_condition)[0] @@ -2031,7 +2078,12 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St loop_counter += 1 if loop_counter >= max_iterations: - raise LoopLimitExceededError("Loop exceeded max allowed iterations") + raise_qasm3_error( + "Loop exceeded max allowed iterations", + err_type=LoopLimitExceededError, + error_node=statement, + span=statement.span, + ) return result From 958e9dd87c0a0599e2e2329639e597634d347f33 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:05:49 +0530 Subject: [PATCH 10/22] Update exceptions.py --- src/pyqasm/exceptions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 7f351576..6487fa2b 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -54,6 +54,23 @@ class LoopLimitExceededError(PyQasmError): def __init__(self, message: str = "Loop limit exceeded."): super().__init__(message) +class LoopControlSignal(Exception): + """Base class for loop control signals like break and continue. + This class is used to signal control flow changes within loops during AST traversal.""" + def __init__(self, signal_type: str): + assert signal_type in ("break", "continue") + self.signal_type = signal_type + +class BreakSignal(LoopControlSignal): + """Signal to break out of a loop during AST traversal.""" + def __init__(self, msg): + super().__init__("break") + +class ContinueSignal(LoopControlSignal): + """Signal to continue to the next iteration of a loop during AST traversal.""" + def __init__(self, msg): + super().__init__("continue") + def raise_qasm3_error( message: Optional[str] = None, err_type: Type[Exception] = ValidationError, From 748f8324c28bf0419b0f8fc3fb422169359647d0 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:07:47 +0530 Subject: [PATCH 11/22] Update visitor.py --- src/pyqasm/visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index d36bab35..2f4e0253 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2045,7 +2045,7 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St result = [] loop_counter = 0 - max_iterations = 5 + max_iterations = self._loop_limit if self._condition_depends_on_measurement(statement.while_condition): raise_qasm3_error( From 738bbf3968bc777af0edf3958ee98cf1e85cb9d5 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:13:16 +0530 Subject: [PATCH 12/22] Update test_while.py --- tests/qasm3/test_while.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py index f6c9e422..1f69d620 100644 --- a/tests/qasm3/test_while.py +++ b/tests/qasm3/test_while.py @@ -168,22 +168,6 @@ def test_while_loop_scope(): result.unroll() check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") - -def test_while_loop_quantum_measurement(): - """Test that while loop with quantum measurement in condition raises error.""" - qasm_str = """ - OPENQASM 3.0; - qubit q; - bit c; - c = measure q; - while (c) { - h q; - c = measure q; - } - """ - result = loads(qasm_str) - result.unroll() - def test_while_loop_limit_exceeded(): """Test that exceeding the loop limit raises LoopLimitExceeded.""" qasm_str = """ @@ -210,7 +194,7 @@ def test_while_loop_quantum_measurement(): c = measure q; } """ - with pytest.raises(ValidationError, match="Cannot unroll while-loop with condition depending on quantum measurement result."): + with pytest.raises(ValidationError, match="quantum measurement"): result = loads(qasm_str) result.unroll() From 7fa72a72d00710986dd23eed73a866f4c1cccf8f Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:14:51 +0530 Subject: [PATCH 13/22] Update exceptions.py --- src/pyqasm/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 6487fa2b..d33ac23d 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -63,12 +63,12 @@ def __init__(self, signal_type: str): class BreakSignal(LoopControlSignal): """Signal to break out of a loop during AST traversal.""" - def __init__(self, msg): + def __init__(self, msg: Optional[str] = None): super().__init__("break") class ContinueSignal(LoopControlSignal): """Signal to continue to the next iteration of a loop during AST traversal.""" - def __init__(self, msg): + def __init__(self, msg: Optional[str] = None): super().__init__("continue") def raise_qasm3_error( From 439a494be9039498794f3b302f12f82231b0af8d Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:25:37 +0530 Subject: [PATCH 14/22] Update visitor.py --- src/pyqasm/visitor.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 2f4e0253..bc839f6a 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -30,7 +30,11 @@ from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable -from pyqasm.exceptions import ValidationError, raise_qasm3_error, LoopLimitExceededError, LoopControlSignal, BreakSignal, ContinueSignal +from pyqasm.exceptions import ( + ValidationError, + raise_qasm3_error, + LoopLimitExceededError, + LoopControlSignal, BreakSignal, ContinueSignal) from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS @@ -888,11 +892,13 @@ def _visit_basic_gate_operation( def _visit_break(self, statement: qasm3_ast.BreakStatement) -> list[qasm3_ast.Statement]: raise_qasm3_error( err_type=BreakSignal, + error_node=statement, ) def _visit_continue(self, statement: qasm3_ast.ContinueStatement) -> list[qasm3_ast.Statement]: raise_qasm3_error( err_type=ContinueSignal, + error_node=statement, ) def _visit_custom_gate_operation( @@ -1627,7 +1633,7 @@ def _visit_classical_assignment( return [] return statements - + def _evaluate_array_initialization( self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: Any ) -> np.ndarray: @@ -2006,30 +2012,26 @@ def _visit_function_call( return return_value, [] return return_value, result - + def _condition_depends_on_measurement(self, condition: qasm3_ast.Expression) -> bool: - """Recursively check whether the given condition depends on a classical register set by measurement.""" + """Recursively check if the condition depends on a classical register set by measurement.""" def depends(expr: qasm3_ast.Expression) -> bool: if isinstance(expr, qasm3_ast.Identifier): return expr.name in self._measurement_set - elif isinstance(expr, qasm3_ast.IndexExpression): + if isinstance(expr, qasm3_ast.IndexExpression): # Check if the collection being indexed is in the measurement set if isinstance(expr.collection, qasm3_ast.Identifier): return expr.collection.name in self._measurement_set return depends(expr.collection) or depends(expr.index) - elif isinstance(expr, qasm3_ast.BinaryExpression): + if isinstance(expr, qasm3_ast.BinaryExpression): return depends(expr.lhs) or depends(expr.rhs) - elif isinstance(expr, qasm3_ast.UnaryExpression): + if isinstance(expr, qasm3_ast.UnaryExpression): return depends(expr.expression) - elif isinstance(expr, qasm3_ast.FunctionCall): - return any(depends(arg) for arg in expr.arguments) - return False - return depends(condition) @@ -2038,9 +2040,9 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St """ Input: statement (qasm3_ast.WhileLoop) - the while-loop AST node Output: - list[qasm3_ast.Statement] - flattened/unrolled statements + list[qasm3_ast.Statement] - flattened/unrolled statements Raises: - ValidationError - if loop condition is non-classical or dynamic """ + ValidationError - if loop condition is non-classical or dynamic""" result = [] @@ -2054,7 +2056,6 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St span=statement.span, ) - while True: cond_value = Qasm3ExprEvaluator.evaluate_expression(statement.while_condition)[0] if not cond_value: From 00d96170c2adcfbd5a6d5024f045a2129a10d965 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:30:13 +0530 Subject: [PATCH 15/22] Update exceptions.py --- src/pyqasm/exceptions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index d33ac23d..88866307 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -64,11 +64,15 @@ def __init__(self, signal_type: str): class BreakSignal(LoopControlSignal): """Signal to break out of a loop during AST traversal.""" def __init__(self, msg: Optional[str] = None): - super().__init__("break") + if msg is None: + msg = "break" + super().__init__(msg) class ContinueSignal(LoopControlSignal): """Signal to continue to the next iteration of a loop during AST traversal.""" def __init__(self, msg: Optional[str] = None): + if msg is None: + msg = "continue" super().__init__("continue") def raise_qasm3_error( From ef8ec1e2d778b2ae6a65a470e5cd7bc1dde39a11 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:32:30 +0530 Subject: [PATCH 16/22] Update visitor.py --- src/pyqasm/visitor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index bc839f6a..f1618b10 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -31,9 +31,9 @@ from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable from pyqasm.exceptions import ( - ValidationError, - raise_qasm3_error, - LoopLimitExceededError, + ValidationError, + raise_qasm3_error, + LoopLimitExceededError, LoopControlSignal, BreakSignal, ContinueSignal) from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS @@ -2031,6 +2031,7 @@ def depends(expr: qasm3_ast.Expression) -> bool: if isinstance(expr, qasm3_ast.UnaryExpression): return depends(expr.expression) + return False return depends(condition) @@ -2043,7 +2044,7 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St list[qasm3_ast.Statement] - flattened/unrolled statements Raises: ValidationError - if loop condition is non-classical or dynamic""" - + result = [] loop_counter = 0 From 7224ede420bb7b2610538faca07b36d38b4ac77a Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:37:17 +0530 Subject: [PATCH 17/22] Update visitor.py --- src/pyqasm/visitor.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index f1618b10..f3f32c29 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -29,15 +29,28 @@ from openqasm3.printer import dumps from pyqasm.analyzer import Qasm3Analyzer -from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable +from pyqasm.elements import ( + ClbitDepthNode, + Context, + InversionOp, + QubitDepthNode, + Variable, +) from pyqasm.exceptions import ( + BreakSignal, + ContinueSignal, + LoopControlSignal, + LoopLimitExceededError, ValidationError, raise_qasm3_error, - LoopLimitExceededError, - LoopControlSignal, BreakSignal, ContinueSignal) +) from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS -from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS +from pyqasm.maps.expressions import ( + ARRAY_TYPE_MAP, + CONSTANTS_MAP, + MAX_ARRAY_DIMENSIONS, +) from pyqasm.maps.gates import ( map_qasm_ctrl_op_to_callable, map_qasm_inv_op_to_callable, From 8314ce31ae53fc642c0b930e602e0371191fb190 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:38:18 +0530 Subject: [PATCH 18/22] Update test_while.py --- tests/qasm3/test_while.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py index 1f69d620..e608e4ff 100644 --- a/tests/qasm3/test_while.py +++ b/tests/qasm3/test_while.py @@ -19,11 +19,11 @@ """ import pytest + from pyqasm import loads from pyqasm.exceptions import LoopLimitExceededError, ValidationError -from tests.utils import ( - check_single_qubit_gate_op -) + +from tests.utils import check_single_qubit_gate_op def test_while_loop_with_continue(): """Test a while loop with break and continue statements.""" From 2b4282b3cc2124ffcc340b05f9566e3ad336d2cf Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:53:43 +0530 Subject: [PATCH 19/22] Update test_while.py From 8dcce92dc630365331886eca2829c5902527be1d Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:48:33 +0530 Subject: [PATCH 20/22] Update test_while.py --- tests/qasm3/test_while.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py index e608e4ff..42318127 100644 --- a/tests/qasm3/test_while.py +++ b/tests/qasm3/test_while.py @@ -78,17 +78,19 @@ def test_while_loop_unroll_qasm_output(): """Test that unrolling a while loop produces the expected QASM string.""" qasm_str = """ OPENQASM 3.0; - qubit q; + qubit[4] q; int i = 0; - while (i < 2) { - h q; + while (i < 3) { + h q[i]; + cx q[i], q[i+1]; i += 1; } + """ result = loads(qasm_str) result.unroll() - # Validate number of h q operations - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 1, 2], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") def test_empty_while_loop_ignored(): From 3709c3324d368dfc1c0e110fc1a97bf79d911071 Mon Sep 17 00:00:00 2001 From: Bhagyasree Yadlapalli <133559120+bhagyasreey@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:51:46 +0530 Subject: [PATCH 21/22] Update test_while.py --- tests/qasm3/test_while.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py index 42318127..3af5dada 100644 --- a/tests/qasm3/test_while.py +++ b/tests/qasm3/test_while.py @@ -23,7 +23,7 @@ from pyqasm import loads from pyqasm.exceptions import LoopLimitExceededError, ValidationError -from tests.utils import check_single_qubit_gate_op +from tests.utils import check_single_qubit_gate_op, check_two_qubit_gate_op def test_while_loop_with_continue(): """Test a while loop with break and continue statements.""" From c18d7545f4efd4320b945f82c29de6254f224a7e Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 16 Jun 2025 11:49:54 +0530 Subject: [PATCH 22/22] finalize PR --- CHANGELOG.md | 32 ++++++++++++++++ src/pyqasm/__init__.py | 3 +- src/pyqasm/analyzer.py | 25 ++++++++++++ src/pyqasm/exceptions.py | 8 ++++ src/pyqasm/modules/base.py | 1 + src/pyqasm/visitor.py | 78 +++++++++++++++----------------------- tests/qasm3/test_while.py | 12 ++++-- 7 files changed, 106 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdecd93..2cd6b34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,38 @@ Types of changes: ### Added - Added the `pulse` extra dependency to the `pyproject.toml` file, which includes the `openpulse` package. This allows users to install pulse-related functionality when needed. ([#195](https://github.com/qBraid/pyqasm/pull/195)) +- Added support for unrolling `while` loops with compile time condition evaluation. Users can now use `unroll` on while loops which do not have conditions depending on quantum measurements. ([#206](https://github.com/qBraid/pyqasm/pull/206)) Eg. - + +```python +import pyqasm + +qasm_str = """ + OPENQASM 3.0; + qubit[4] q; + int i = 0; + while (i < 3) { + h q[i]; + cx q[i], q[i+1]; + i += 1; + } + + """ +result = pyqasm.loads(qasm_str) +result.unroll() +print(result) + +# **Output** + +# OPENQASM 3.0; +# qubit[4] q; +# h q[0]; +# cx q[0], q[1]; +# h q[1]; +# cx q[1], q[2]; +# h q[2]; +# cx q[2], q[3]; + +``` ### Improved / Modified diff --git a/src/pyqasm/__init__.py b/src/pyqasm/__init__.py index 4629a600..c3df07c7 100644 --- a/src/pyqasm/__init__.py +++ b/src/pyqasm/__init__.py @@ -61,13 +61,14 @@ __version__ = version("pyqasm") from .entrypoint import dump, dumps, load, loads -from .exceptions import PyQasmError, QasmParsingError, ValidationError +from .exceptions import LoopLimitExceededError, PyQasmError, QasmParsingError, ValidationError from .modules import Qasm2Module, Qasm3Module, QasmModule from .printer import draw __all__ = [ "PyQasmError", "ValidationError", + "LoopLimitExceededError", "QasmParsingError", "load", "loads", diff --git a/src/pyqasm/analyzer.py b/src/pyqasm/analyzer.py index db113ec1..7c82afd3 100644 --- a/src/pyqasm/analyzer.py +++ b/src/pyqasm/analyzer.py @@ -23,6 +23,7 @@ import numpy as np from openqasm3.ast import ( + BinaryExpression, DiscreteSet, Expression, Identifier, @@ -34,6 +35,7 @@ QuantumMeasurementStatement, RangeDefinition, Span, + UnaryExpression, ) from pyqasm.exceptions import QasmParsingError, ValidationError, raise_qasm3_error @@ -292,3 +294,26 @@ def verify_gate_qubits(gate: QuantumGate, span: Optional[Span] = None): error_node=gate, span=span, ) + + @staticmethod + def condition_depends_on_measurement(condition: Expression, measurement_set: set[str]) -> bool: + """Recursively check if the condition depends on a classical register set by measurement.""" + + def _depends(expr) -> bool: + if isinstance(expr, Identifier): + return expr.name in measurement_set + + if isinstance(expr, IndexExpression): + # Check if the collection being indexed is in the measurement set + if isinstance(expr.collection, Identifier): + return expr.collection.name in measurement_set + return _depends(expr.collection) or _depends(expr.index) + + if isinstance(expr, BinaryExpression): + return _depends(expr.lhs) or _depends(expr.rhs) + + if isinstance(expr, UnaryExpression): + return _depends(expr.expression) + return False + + return _depends(condition) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 88866307..dfb3b161 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -48,33 +48,41 @@ class QasmParsingError(QASM3ParsingError): """An error raised by the AST visitor during the AST-generation phase. This is raised in cases where the given program could not be correctly parsed.""" + class LoopLimitExceededError(PyQasmError): """Exception raised when a loop limit is exceeded during unrolling or other operations.""" def __init__(self, message: str = "Loop limit exceeded."): super().__init__(message) + class LoopControlSignal(Exception): """Base class for loop control signals like break and continue. This class is used to signal control flow changes within loops during AST traversal.""" + def __init__(self, signal_type: str): assert signal_type in ("break", "continue") self.signal_type = signal_type + class BreakSignal(LoopControlSignal): """Signal to break out of a loop during AST traversal.""" + def __init__(self, msg: Optional[str] = None): if msg is None: msg = "break" super().__init__(msg) + class ContinueSignal(LoopControlSignal): """Signal to continue to the next iteration of a loop during AST traversal.""" + def __init__(self, msg: Optional[str] = None): if msg is None: msg = "continue" super().__init__("continue") + def raise_qasm3_error( message: Optional[str] = None, err_type: Type[Exception] = ValidationError, diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index e33df77f..c8c1e2f5 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -528,6 +528,7 @@ def unroll(self, **kwargs): **kwargs: Additional arguments to pass to the QasmVisitor. external_gates (list[str]): List of gates that should not be unrolled. unroll_barriers (bool): If True, barriers will be unrolled. Defaults to True. + max_loop_iters (int): Max number of iterations for unrolling loops. Defaults to 1e9. check_only (bool): If True, only check the program without executing it. Defaults to False. diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index f3f32c29..51648bf1 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -79,12 +79,13 @@ class QasmVisitor: check_only (bool): If True, only check the program without executing it. Defaults to False. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, module, check_only: bool = False, external_gates: list[str] | None = None, unroll_barriers: bool = True, + max_loop_iters: int = int(1e9), ): self._module = module self._scope: deque = deque([{}]) @@ -109,9 +110,9 @@ def __init__( self._in_branching_statement: int = 0 self._is_branch_qubits: set[tuple[str, int]] = set() self._is_branch_clbits: set[tuple[str, int]] = set() - self._measurement_set = set() + self._measurement_set: set[str] = set() self._init_utilities() - self._loop_limit = 1e9 + self._loop_limit = max_loop_iters def _init_utilities(self): """Initialize the utilities for the visitor.""" @@ -902,16 +903,16 @@ def _visit_basic_gate_operation( return result - def _visit_break(self, statement: qasm3_ast.BreakStatement) -> list[qasm3_ast.Statement]: + def _visit_break(self, statement: qasm3_ast.BreakStatement) -> None: raise_qasm3_error( - err_type=BreakSignal, - error_node=statement, + err_type=BreakSignal, + error_node=statement, ) - def _visit_continue(self, statement: qasm3_ast.ContinueStatement) -> list[qasm3_ast.Statement]: + def _visit_continue(self, statement: qasm3_ast.ContinueStatement) -> None: raise_qasm3_error( - err_type=ContinueSignal, - error_node=statement, + err_type=ContinueSignal, + error_node=statement, ) def _visit_custom_gate_operation( @@ -2026,49 +2027,30 @@ def _visit_function_call( return return_value, result - def _condition_depends_on_measurement(self, condition: qasm3_ast.Expression) -> bool: - """Recursively check if the condition depends on a classical register set by measurement.""" - - def depends(expr: qasm3_ast.Expression) -> bool: - if isinstance(expr, qasm3_ast.Identifier): - return expr.name in self._measurement_set - - if isinstance(expr, qasm3_ast.IndexExpression): - # Check if the collection being indexed is in the measurement set - if isinstance(expr.collection, qasm3_ast.Identifier): - return expr.collection.name in self._measurement_set - return depends(expr.collection) or depends(expr.index) - - if isinstance(expr, qasm3_ast.BinaryExpression): - return depends(expr.lhs) or depends(expr.rhs) - - if isinstance(expr, qasm3_ast.UnaryExpression): - return depends(expr.expression) - return False - - return depends(condition) - - def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.Statement]: + """Visit a while-loop element. - """ Input: - statement (qasm3_ast.WhileLoop) - the while-loop AST node - Output: - list[qasm3_ast.Statement] - flattened/unrolled statements - Raises: - ValidationError - if loop condition is non-classical or dynamic""" + Args: + statement (qasm3_ast.WhileLoop) - the while-loop AST node + Returns: + list[qasm3_ast.Statement] - flattened/unrolled statements + Raises: + ValidationError - if loop condition is non-classical or dynamic + LoopLimitExceededError - if the loop exceeds the maximum limit""" result = [] loop_counter = 0 max_iterations = self._loop_limit - if self._condition_depends_on_measurement(statement.while_condition): + if Qasm3Analyzer.condition_depends_on_measurement( + statement.while_condition, self._measurement_set + ): raise_qasm3_error( - "Cannot unroll while-loop with condition depending on quantum measurement result.", - error_node=statement, - span=statement.span, - ) + "Cannot unroll while-loop with condition depending on quantum measurement result.", + error_node=statement, + span=statement.span, + ) while True: cond_value = Qasm3ExprEvaluator.evaluate_expression(statement.while_condition)[0] @@ -2094,11 +2076,11 @@ def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> list[qasm3_ast.St loop_counter += 1 if loop_counter >= max_iterations: raise_qasm3_error( - "Loop exceeded max allowed iterations", - err_type=LoopLimitExceededError, - error_node=statement, - span=statement.span, - ) + "Loop exceeded max allowed iterations", + err_type=LoopLimitExceededError, + error_node=statement, + span=statement.span, + ) return result diff --git a/tests/qasm3/test_while.py b/tests/qasm3/test_while.py index 3af5dada..7b735b1e 100644 --- a/tests/qasm3/test_while.py +++ b/tests/qasm3/test_while.py @@ -22,9 +22,9 @@ from pyqasm import loads from pyqasm.exceptions import LoopLimitExceededError, ValidationError - from tests.utils import check_single_qubit_gate_op, check_two_qubit_gate_op + def test_while_loop_with_continue(): """Test a while loop with break and continue statements.""" qasm_str = """ @@ -48,9 +48,9 @@ def test_while_loop_with_continue(): result = loads(qasm_str) result.unroll() - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 2], "h") + def test_while_loop_with_break(): qasm_str = """ OPENQASM 3.0; @@ -170,19 +170,21 @@ def test_while_loop_scope(): result.unroll() check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "h") + def test_while_loop_limit_exceeded(): """Test that exceeding the loop limit raises LoopLimitExceeded.""" qasm_str = """ OPENQASM 3.0; qubit q; int i = 0; - while (i < 1e10) { + while (i < 1e5) { i += 1; } """ result = loads(qasm_str) with pytest.raises(LoopLimitExceededError): - result.unroll() + result.unroll(max_loop_iters=1e3) + def test_while_loop_quantum_measurement(): """Test that while loop with quantum measurement in condition raises error.""" @@ -200,6 +202,7 @@ def test_while_loop_quantum_measurement(): result = loads(qasm_str) result.unroll() + def test_while_loop_measurement_complex_condition(): qasm_str = """ OPENQASM 3.0; @@ -215,6 +218,7 @@ def test_while_loop_measurement_complex_condition(): result = loads(qasm_str) result.unroll() + def test_while_loop_measurement_binary_expr(): qasm_str = """ OPENQASM 3.0;