diff --git a/CHANGELOG.md b/CHANGELOG.md index 743c6df..0da3e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Types of changes: ### Fixed - Fixed multiple axes error in circuit visualization of decomposable gates in `draw` method. ([#209](https://github.com/qBraid/pyqasm/pull/210)) - Fixed depth calculation for decomposable gates by computing depth of each constituent quantum gate.([#211](https://github.com/qBraid/pyqasm/pull/211)) +- Optimized statement copying in `_visit_function_call` with shallow-copy fallback to deepcopy and added `max_loop_iters` loop‐limit check in for loops.([#223](https://github.com/qBraid/pyqasm/pull/223)) ### Dependencies ### Other diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index b6e4796..ef71d35 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1924,6 +1924,16 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.St span=statement.span, ) + # Check if the loop range exceeds the maximum allowed iterations + if len(irange) > self._loop_limit: + raise_qasm3_error( + # pylint: disable-next=line-too-long + f"Loop range '{len(irange)-1}' exceeded max allowed '{self._loop_limit}' iterations", + err_type=LoopLimitExceededError, + error_node=statement, + span=statement.span, + ) + i: Optional[Variable] # will store iteration Variable to update to loop scope result = [] @@ -2068,9 +2078,12 @@ def _visit_function_call( result = [] for function_op in subroutine_def.body: if isinstance(function_op, qasm3_ast.ReturnStatement): - return_statement = copy.deepcopy(function_op) + return_statement = copy.copy(function_op) break - result.extend(self.visit_statement(copy.deepcopy(function_op))) + try: + result.extend(self.visit_statement(copy.copy(function_op))) + except (TypeError, copy.Error): + result.extend(self.visit_statement(copy.deepcopy(function_op))) return_value = None if return_statement: diff --git a/tests/qasm3/test_loop.py b/tests/qasm3/test_loop.py index ad7a330..bcb71c4 100644 --- a/tests/qasm3/test_loop.py +++ b/tests/qasm3/test_loop.py @@ -20,7 +20,7 @@ import pytest from pyqasm.entrypoint import loads -from pyqasm.exceptions import ValidationError +from pyqasm.exceptions import LoopLimitExceededError, ValidationError from tests.utils import ( check_single_qubit_gate_op, check_single_qubit_rotation_op, @@ -332,3 +332,38 @@ def test_convert_qasm3_for_loop_unsupported_type(caplog): assert "Error at line 9, column 16" in caplog.text assert 'for bit b in "001"' in caplog.text + + +def test_for_loop_limit_exceeded(): + """Test that exceeding the loop limit raises LoopLimitExceededError for for loops.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + + for int i in [0:1000] { + h q[0]; + } + """ + result = loads(qasm_str) + with pytest.raises(LoopLimitExceededError): + result.unroll(max_loop_iters=100) + + +def test_for_loop_discrete_set_limit_exceeded(): + """Test that exceeding the loop limit raises LoopLimitExceededError + for for loops with discrete sets.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + + for int i in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} { + h q[0]; + } + """ + result = loads(qasm_str) + with pytest.raises(LoopLimitExceededError): + result.unroll(max_loop_iters=10)