diff --git a/.pylintrc b/.pylintrc index 26998d8..44744d8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -104,10 +104,6 @@ recursive=no # source root. source-roots= -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ddc05..3a646b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Types of changes: ### Removed ### Fixed +- Fixed classical register declarations not being visible inside `box` scope, causing "Missing clbit register declaration" errors for measurement statements inside box blocks. ([#306](https://github.com/qBraid/pyqasm/pull/306)) ### Dependencies diff --git a/src/pyqasm/scope.py b/src/pyqasm/scope.py index 12484ee..ff7de2b 100644 --- a/src/pyqasm/scope.py +++ b/src/pyqasm/scope.py @@ -149,19 +149,19 @@ def check_in_scope(self, var_name: str) -> bool: curr_scope = self.get_curr_scope() if self.in_global_scope(): return var_name in global_scope - if ( - self.in_function_scope() - or self.in_gate_scope() - or self.in_box_scope() - or self.in_pulse_scope() - ): + if self.in_function_scope() or self.in_gate_scope() or self.in_pulse_scope(): if var_name in curr_scope: return True if var_name in global_scope: return global_scope[var_name].is_constant or global_scope[var_name].is_qubit - if self.in_block_scope(): + if self.in_block_scope() or self.in_box_scope(): + in_box = self.in_box_scope() for scope, context in zip(reversed(self._scope), reversed(self._context)): - if context != Context.BLOCK: + if context == Context.BOX: + in_box = True + if context not in (Context.BLOCK, Context.BOX): + if in_box and var_name in scope: + return scope[var_name].is_constant or scope[var_name].is_qubit return var_name in scope if var_name in scope: return True @@ -193,12 +193,7 @@ def get_from_visible_scope(self, var_name: str) -> Variable | None: curr_scope = self.get_curr_scope() if self.in_global_scope(): return global_scope.get(var_name, None) - if ( - self.in_function_scope() - or self.in_gate_scope() - or self.in_box_scope() - or self.in_pulse_scope() - ): + if self.in_function_scope() or self.in_gate_scope() or self.in_pulse_scope(): if var_name in curr_scope: return curr_scope[var_name] if var_name in global_scope and ( @@ -207,10 +202,10 @@ def get_from_visible_scope(self, var_name: str) -> Variable | None: # we also need to return the variable if it is a constant or qubit # in the global scope, as it can be used in the function or gate return global_scope[var_name] - if self.in_block_scope(): + if self.in_block_scope() or self.in_box_scope(): var_found = None for scope, context in zip(reversed(self._scope), reversed(self._context)): - if context != Context.BLOCK: + if context not in (Context.BLOCK, Context.BOX): var_found = scope.get(var_name, None) break if var_name in scope: diff --git a/tests/qasm3/test_box.py b/tests/qasm3/test_box.py index d8ef0d1..c566c3b 100644 --- a/tests/qasm3/test_box.py +++ b/tests/qasm3/test_box.py @@ -232,3 +232,122 @@ def test_box_statement_error(qasm_code, error_message, error_span, caplog): loads(qasm_code).unroll() assert error_message in str(err.value) assert error_span in caplog.text + + +def test_box_measurement_with_classical_register(): + """Measurement inside a box should have access to classical registers + declared in the enclosing (global) scope. + + Regression: box scope was treated like function/gate scope, restricting + access to only constants and qubits from global scope. Classical registers + were invisible, causing 'Missing clbit register declaration' errors. + See: https://github.com/qBraid/pyqasm/issues/302 + """ + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[3] c; + + box [100ns] { + h q[0]; + c[1] = measure q[1]; + } + """ + result = loads(qasm_str) + result.validate() + + +def test_box_measurement_with_enclosing_block_scope(): + """Measurement inside a box nested within a non-global scope (e.g. an + if-block) should still have access to classical registers declared in + the enclosing global scope. + + Complements test_box_measurement_with_classical_register by ensuring the + scope walker traverses intermediate BLOCK contexts to reach the global + scope where the classical register is declared. + """ + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[3] c; + + if (true) { + box { + h q[0]; + c[1] = measure q[1]; + } + } + """ + result = loads(qasm_str) + result.validate() + + +def test_box_measurement_with_block_scoped_register(): + """Measurement inside a box nested within a block scope should have access + to a classical register declared in the immediate parent block scope.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + + if (true) { + bit[3] c; + box { + h q[0]; + c[1] = measure q[1]; + } + } + """ + result = loads(qasm_str) + result.validate() + + +def test_box_measurement_with_multiple_classical_registers(): + """Measurement inside a box should have access to multiple classical + registers declared in the enclosing (global) scope.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[3] c0; + bit[2] c1; + + box { + c0[0] = measure q[0]; + c0[2] = measure q[2]; + c1[1] = measure q[3]; + } + """ + result = loads(qasm_str) + result.validate() + + +def test_multiple_box_statements(): + """Multiple box statements in the same program should each have access + to classical registers from the enclosing scope.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + + box [50ns] { + h q[0]; + c[0] = measure q[0]; + } + + box [100ns] { + cx q[1], q[2]; + c[1] = measure q[1]; + c[2] = measure q[2]; + } + + box { + h q[3]; + c[3] = measure q[3]; + } + """ + result = loads(qasm_str) + result.validate()