Skip to content

Fix classical register visibility inside box scope#306

Merged
TheGupta2012 merged 6 commits intomainfrom
fix/box-scope-classical-registers
Apr 8, 2026
Merged

Fix classical register visibility inside box scope#306
TheGupta2012 merged 6 commits intomainfrom
fix/box-scope-classical-registers

Conversation

@ryanhill1
Copy link
Copy Markdown
Member

@ryanhill1 ryanhill1 commented Mar 26, 2026

Summary

  • Fix box blocks being treated as isolated scopes (like gate/function), preventing access to classical registers from the enclosing scope
  • c[1] = measure q[1]; inside a box block now correctly resolves the classical register c declared at global scope

Details

In scope.py, get_from_visible_scope() grouped in_box_scope() with in_function_scope() / in_gate_scope() / in_pulse_scope(), which only allows access to constants and qubits from global scope. Classical registers were invisible, causing "Missing clbit register declaration" errors.

Per the OpenQASM 3.0 spec, box is a timing directive that executes in the enclosing scope — not an isolated scope like a gate or function definition. This PR moves in_box_scope() to the block scope group, which has full parent scope visibility.

Closes #302

Summary by CodeRabbit

  • Bug Fixes
    • Classical register declarations are now visible inside box scopes; measurements in box blocks can reference registers declared in enclosing or global scopes without validation errors.
  • Documentation
    • Changelog updated to document the above fix.
  • Tests
    • Added tests covering measurements to classical registers from within box scopes (nested blocks, multiple registers, multiple boxes).
  • Chores
    • Removed a pylint suggestion-mode setting.

@ryanhill1 ryanhill1 requested a review from TheGupta2012 as a code owner March 26, 2026 22:21
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Walkthrough

Resolve scope lookup so classical registers declared in enclosing scopes are visible inside box blocks; update scope resolution logic, add unit tests covering measurements inside box, and document the fix in the changelog.

Changes

Cohort / File(s) Summary
Scope visibility logic
src/pyqasm/scope.py
Modified ScopeManager.get_from_visible_scope() to remove BOX from the prior branch and include in_box_scope() in the fallback reverse-scope lookup (in_block_scope() or in_box_scope()), allowing BOX-context to resolve outer classical register declarations via the same reverse iteration.
Tests
tests/qasm3/test_box.py
Added multiple tests (test_box_measurement_with_classical_register, test_box_measurement_with_enclosing_block_scope, test_box_measurement_with_block_scoped_register, test_box_measurement_with_multiple_classical_registers, test_multiple_box_statements) that validate measurement statements inside box blocks can reference classical registers from enclosing/global or BLOCK scopes.
Changelog & config
CHANGELOG.md, .pylintrc
Added changelog entry under Unreleased → Fixed documenting the box classical-register visibility bug; removed suggestion-mode=yes from .pylintrc.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix classical register visibility inside box scope' directly and clearly describes the main change: enabling classical registers declared in outer scopes to be accessible within box blocks.
Linked Issues check ✅ Passed The PR successfully addresses issue #302 by modifying scope resolution in scope.py to allow box blocks to inherit parent-scope visibility for classical registers, enabling measurement statements to access outer-scope classical register declarations.
Out of Scope Changes check ✅ Passed All changes are directly related to the scope fix objective: scope.py implements the core fix, test_box.py validates the functionality, CHANGELOG.md documents it, and .pylintrc removal is a minor configuration cleanup.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/box-scope-classical-registers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ryanhill1 added a commit that referenced this pull request Mar 26, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ryanhill1 and others added 2 commits March 26, 2026 17:22
Box blocks were treated the same as function/gate scopes in
`get_from_visible_scope()`, restricting access to only constants and
qubits from the global scope. This caused measurement statements inside
box blocks to fail with "Missing clbit register declaration" even when
the classical register was declared at the top level.

Per the OpenQASM 3.0 spec, `box` is a timing directive that executes
in the enclosing scope, not an isolated scope. Move `in_box_scope()`
from the restricted group (function/gate/pulse) to the block group
which has full parent scope visibility.

Closes #302

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ryanhill1 ryanhill1 force-pushed the fix/box-scope-classical-registers branch from 27ad8f6 to 0ba9da2 Compare March 26, 2026 22:23
Remove deprecated `suggestion-mode` option from .pylintrc (removed in
pylint 3.3.0) and reformat multi-line condition in scope.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pyqasm/scope.py (2)

196-221: ⚠️ Potential issue | 🟡 Minor

Run Black on this file before merge.

black --check is still failing on src/pyqasm/scope.py, so CI will remain red until this file is reformatted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyqasm/scope.py` around lines 196 - 221, Run the Python formatter Black
on src/pyqasm/scope.py to satisfy CI: reformat the file (e.g., run black
src/pyqasm/scope.py or black .) and commit the changes so the function blocks
involving in_function_scope, in_gate_scope, in_pulse_scope, in_block_scope,
in_box_scope and the loop over self._scope / self._context (Context.BLOCK) are
reformatted per Black's style; no logic changes required, just apply Black and
update the commit.

196-221: ⚠️ Potential issue | 🔴 Critical

Do not stop the scope walk at Context.BOX.

src/pyqasm/visitor.py, Lines 2912-2945 always pushes a fresh Context.BOX, so this only fixes GLOBAL -> BOX. In a reachable stack like FUNCTION -> BOX or BLOCK -> BOX, Line 212 breaks on the BOX frame immediately, so parent locals stay invisible and Line 220 falls back straight to global scope, bypassing the real enclosing-scope rules. It also leaves get_from_visible_scope() inconsistent with check_in_scope() on Lines 152-162 for the same symbol. Please make BOX transparent in the ancestor walk and share that rule across both visibility helpers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyqasm/scope.py` around lines 196 - 221, The ancestor-walk must treat
Context.BOX as transparent: in the scope-walking logic inside
get_from_visible_scope (the function containing the shown loop over
reversed(self._scope)/reversed(self._context)), do not break when encountering a
BOX frame—skip BOX frames and continue searching parent scopes for var_name
instead of immediately falling back to global; make this behavior identical to
check_in_scope (lines ~152-162) by sharing the BOX-transparency rule (extract a
small helper or reuse the check_in_scope logic) so both functions treat
Context.BOX the same and parent locals (e.g., FUNCTION->BOX or BLOCK->BOX)
remain visible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/qasm3/test_box.py`:
- Around line 237-258: Add a test that exercises BOX visibility when the
enclosing scope is non-global: create a new test (e.g.,
test_box_measurement_with_enclosing_block_scope) which defines a non-global
parent scope (like a named gate/block or a nested block/function) that declares
a classical register (bit[...] c;) and inside that parent scope place a box that
performs c[...] = measure q[...]; then call loads(qasm_str) and
result.validate() to assert no 'Missing clbit register' error. This complements
the existing test and ensures the visitor behavior (see src/pyqasm/visitor.py
handling that always pushes Context.BOX) allows BOX to see classical registers
from an enclosing BLOCK/FUNCTION scope as well as GLOBAL.

---

Outside diff comments:
In `@src/pyqasm/scope.py`:
- Around line 196-221: Run the Python formatter Black on src/pyqasm/scope.py to
satisfy CI: reformat the file (e.g., run black src/pyqasm/scope.py or black .)
and commit the changes so the function blocks involving in_function_scope,
in_gate_scope, in_pulse_scope, in_block_scope, in_box_scope and the loop over
self._scope / self._context (Context.BLOCK) are reformatted per Black's style;
no logic changes required, just apply Black and update the commit.
- Around line 196-221: The ancestor-walk must treat Context.BOX as transparent:
in the scope-walking logic inside get_from_visible_scope (the function
containing the shown loop over reversed(self._scope)/reversed(self._context)),
do not break when encountering a BOX frame—skip BOX frames and continue
searching parent scopes for var_name instead of immediately falling back to
global; make this behavior identical to check_in_scope (lines ~152-162) by
sharing the BOX-transparency rule (extract a small helper or reuse the
check_in_scope logic) so both functions treat Context.BOX the same and parent
locals (e.g., FUNCTION->BOX or BLOCK->BOX) remain visible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d49b00dc-ed0d-48f6-a74b-ed8bd2311af5

📥 Commits

Reviewing files that changed from the base of the PR and between 113df6a and 0ba9da2.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/pyqasm/scope.py
  • tests/qasm3/test_box.py

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Test that a box nested inside a non-global scope (e.g. an if-block)
can still access classical registers from the enclosing global scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pyqasm/scope.py (2)

152-168: ⚠️ Potential issue | 🟡 Minor

BOX scope handling is inconsistent between check_in_scope and get_from_visible_scope.

check_in_scope (lines 152-161) treats BOX like function/gate/pulse scope, restricting global scope access to constants and qubits. However, get_from_visible_scope (line 205) treats BOX like BLOCK scope and allows unrestricted access to all global variables.

This inconsistency can cause subtle bugs: check_in_scope returns False for a classical register in global scope inside BOX, but get_from_visible_scope successfully returns the same variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyqasm/scope.py` around lines 152 - 168, check_in_scope treats
Context.BOX like function/gate/pulse (restricting global access) but
get_from_visible_scope does not; update get_from_visible_scope to handle
Context.BOX the same way as in_function_scope/in_gate_scope/in_pulse_scope: when
iterating reversed(self._scope)/reversed(self._context) in
get_from_visible_scope, detect Context.BOX and if a var_name is found in
global_scope only return it when global_scope[var_name].is_constant or
global_scope[var_name].is_qubit (same logic used in check_in_scope), otherwise
continue/search or raise as appropriate so BOX visibility behavior is consistent
with check_in_scope.

205-217: ⚠️ Potential issue | 🟠 Major

Loop breaks prematurely when BOX is nested inside a BLOCK scope, preventing access to BLOCK-scoped variables.

When the current context is BOX, the first loop iteration encounters context=BOX which is != Context.BLOCK, causing an immediate break. This prevents the scope walker from finding variables declared in intermediate BLOCK scopes (e.g., classical variables declared inside if-blocks).

For example, with GLOBAL → BLOCK → BOX:

  • Reversed contexts: [BOX, BLOCK, GLOBAL]
  • 1st iteration: context=BOX, condition != BLOCK is True → breaks immediately
  • block_scope is never searched

The existing test test_box_measurement_with_enclosing_block_scope() passes because classical registers are always in GLOBAL scope (handled by fallback). The bug manifests only when accessing variables declared in intermediate BLOCK scopes.

Additionally, the same loop logic issue exists in check_in_scope() (line 164) and set_to_visible_scope() (line 284) methods.

🔧 Proposed fix
         if self.in_block_scope():
             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

Apply the same fix to check_in_scope() (line 164) and set_to_visible_scope() (line 284).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyqasm/scope.py` around lines 205 - 217, The loop that walks reversed
self._scope/_context has its conditions reversed: it breaks on the first
non-BLOCK context (e.g., BOX) before checking intermediate BLOCK scopes, so swap
the branches so BLOCK scopes are inspected first (if context == Context.BLOCK:
if var_name in scope: return scope[var_name]; else continue), and only when
encountering a non-BLOCK context fall back to grabbing scope.get(var_name) and
breaking; apply the same change to check_in_scope() and set_to_visible_scope()
so all three methods handle nested BOX inside BLOCK correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/pyqasm/scope.py`:
- Around line 152-168: check_in_scope treats Context.BOX like
function/gate/pulse (restricting global access) but get_from_visible_scope does
not; update get_from_visible_scope to handle Context.BOX the same way as
in_function_scope/in_gate_scope/in_pulse_scope: when iterating
reversed(self._scope)/reversed(self._context) in get_from_visible_scope, detect
Context.BOX and if a var_name is found in global_scope only return it when
global_scope[var_name].is_constant or global_scope[var_name].is_qubit (same
logic used in check_in_scope), otherwise continue/search or raise as appropriate
so BOX visibility behavior is consistent with check_in_scope.
- Around line 205-217: The loop that walks reversed self._scope/_context has its
conditions reversed: it breaks on the first non-BLOCK context (e.g., BOX) before
checking intermediate BLOCK scopes, so swap the branches so BLOCK scopes are
inspected first (if context == Context.BLOCK: if var_name in scope: return
scope[var_name]; else continue), and only when encountering a non-BLOCK context
fall back to grabbing scope.get(var_name) and breaking; apply the same change to
check_in_scope() and set_to_visible_scope() so all three methods handle nested
BOX inside BLOCK correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b347744-c2c2-4646-b579-f364a72f47f4

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba9da2 and b13d4e7.

📒 Files selected for processing (3)
  • .pylintrc
  • src/pyqasm/scope.py
  • tests/qasm3/test_box.py
💤 Files with no reviewable changes (1)
  • .pylintrc

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 27, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/qasm3/test_box.py`:
- Around line 261-284: Add an additional optional test function named
test_box_measurement_with_block_scoped_register that mirrors the pattern in
test_box_measurement_with_enclosing_block_scope but declares the classical
register inside the if-block (i.e., move the "bit[3] c;" declaration into the if
(true) block) and then performs c[1] = measure q[1]; inside the nested box;
ensure the new test calls loads(qasm_str) and result.validate() just like the
existing test to explicitly cover the immediate parent block scope case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1915a621-9b0d-4501-bbac-9b58a968598b

📥 Commits

Reviewing files that changed from the base of the PR and between b13d4e7 and 56f2493.

📒 Files selected for processing (1)
  • tests/qasm3/test_box.py

The scope walker now traverses through both BLOCK and BOX contexts
when resolving variables, allowing a box nested inside a block (e.g.
an if-statement) to see classical registers declared in that block.
Non-constant, non-qubit globals remain restricted inside box scope.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pyqasm/scope.py`:
- Around line 157-165: The traversal/visibility logic for BOX is inconsistent
between check_in_scope() and get_from_visible_scope(): extract a shared helper
that walks reversed(self._scope) and reversed(self._context), sets an in_box
flag when encountering Context.BOX, and applies the same rule for
non-(Context.BLOCK, Context.BOX) frames: if in_box and var_name in scope then
allow only if scope[var_name].is_constant or scope[var_name].is_qubit, otherwise
return var_name in scope; then have both check_in_scope() and
get_from_visible_scope() call this helper (referencing Context.BOX,
Context.BLOCK, self._scope, self._context, and the var_name lookup) so BOX
visibility is unified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2b1ec21-9861-442b-a17f-69ed2c4fdb07

📥 Commits

Reviewing files that changed from the base of the PR and between 56f2493 and a48ba91.

📒 Files selected for processing (2)
  • src/pyqasm/scope.py
  • tests/qasm3/test_box.py

Copy link
Copy Markdown
Member

@TheGupta2012 TheGupta2012 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good

@TheGupta2012 TheGupta2012 merged commit df2143f into main Apr 8, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measurement inside box cannot access outer-scope classical register

3 participants