Skip to content

Fix classical register visibility inside box scope#306

Open
ryanhill1 wants to merge 4 commits intomainfrom
fix/box-scope-classical-registers
Open

Fix classical register visibility inside box scope#306
ryanhill1 wants to merge 4 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 scope without validation errors.
  • Documentation
    • Changelog updated with the above fix.
  • Tests
    • Added tests validating classical register visibility for measurements inside box and when box is inside a block.
  • 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

Fixes scope resolution so classical register declarations in enclosing scopes are visible inside box blocks; updates scope manager logic, adds tests verifying measurements inside box can reference outer-scope classical registers, and documents the fix in the changelog.

Changes

Cohort / File(s) Summary
Scope visibility logic
src/pyqasm/scope.py
Adjusted ScopeManager.get_from_visible_scope() to include in_box_scope() in the fallback reverse-scope lookup (previously only in_block_scope()), enabling BOX-context to resolve outer classical registers via the same reverse iteration.
Tests
tests/qasm3/test_box.py
Added tests test_box_measurement_with_classical_register and test_box_measurement_with_enclosing_block_scope to assert measurement statements inside box can access classical registers declared in outer/global or enclosing BLOCK scopes.
Changelog & config
CHANGELOG.md, .pylintrc
Documented the bug fix under Unreleased → Fixed in CHANGELOG.md; removed suggestion-mode=yes from .pylintrc.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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' accurately and concisely summarizes the main change: fixing how classical registers are resolved within box blocks.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #302: updating scope resolution logic so box blocks can access classical registers from enclosing scopes, verified by new tests for measurement access and block-nested box scenarios.
Out of Scope Changes check ✅ Passed The .pylintrc change removing 'suggestion-mode' is a minor configuration cleanup unrelated to the scope fix, though the scope.py changes, tests, and CHANGELOG directly address the issue.

✏️ 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

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

2 participants