Skip to content

Support bit and bit[n] subroutine return values, including return measure q - #410

Open
TheGupta2012 wants to merge 2 commits into
feature-385-391-bit-repr-negative-indicesfrom
feature-387-subroutine-bit-return
Open

Support bit and bit[n] subroutine return values, including return measure q#410
TheGupta2012 wants to merge 2 commits into
feature-385-391-bit-repr-negative-indicesfrom
feature-387-subroutine-bit-return

Conversation

@TheGupta2012

Copy link
Copy Markdown
Member

Closes #387

Problem

bit x = f(); crashed with AttributeError: 'NoneType' object has no attribute 'value'validate_return_statement read return_type.size.value unconditionally, and bit carries no size. bit[n] returns did not crash but emitted an unresolved f() call, since a bit declaration survives unrolling while the subroutine definition does not. return measure q; failed earlier still: _visit_measurement never applied the function qubit transform, so any measurement on a formal argument raised "Missing register declaration for 'q'".

Approach

  • The declared return width is evaluated by the caller and passed into validate_return_statement. A cast failure is re-raised as Return type mismatch for subroutine 'f'. Expected bit[2] but got float, naming both types.
  • A bit-returning call is replaced by the expression standing in for its return value: a BitstringLiteral when the register is statically known, a reference when it is not. The substitution is applied to a copy of the declaration/assignment, so the source AST keeps its call and the module can be unrolled again (depth() re-unrolls a copy).
  • _visit_measurement now rewrites its qubit operand innermost scope outwards, exactly as _visit_reset already does. This removes the # TODO: handle in-function measurements.

Temp-bit decision for return measure q;

A measurement has no compile-time value, so the return expression cannot fold to a literal. A temporary bit register is synthesised at the call site (__<fn>_return_<n>, sized from the declared return type) and the measurement targets it; the caller's variable is then initialised from that register:

bit[1] __f_return_0;
__f_return_0[0] = measure q[0];
bit[1] x = __f_return_0;

Reasoning: the emitted program stays valid OpenQASM — a measurement needs a classical target, and folding a runtime value into the caller's variable would emit a stale "0". Each call gets its own numbered register, so repeated calls do not collide.

Assign-then-return (bit c; c = measure q; return c;) needs no temp — the local bit c; already reaches the caller's output, so the identifier is returned by reference. A nested subroutine forwards whichever expression the inner call produced.

Tests

tests/qasm3/subroutines/test_subroutine_returns.py (new module; test_subroutines.py is near the 1000-line pylint cap): -> bit, -> bit[n], return measure q, a multi-qubit return measure q, assign-then-return, assignment to an already-declared caller variable, nested forwarding of both a measured and a literal bit return, repeated calls getting distinct temp registers, unrolling the same module twice, and declared/returned type mismatches (including return measure q; from a void subroutine).

Checks

pytest 842 passed / 3 skipped (baseline 828 / 3). pylint 10.00/10, isort, black, mypy clean; tox -e docs builds.

Stacked on #404 (feature-385-391-bit-repr-negative-indices); rebases onto main once #404 merges.

🤖 Generated with Claude Code

`bit x = f();` crashed with `AttributeError: 'NoneType' object has no
attribute 'value'` because `validate_return_statement` read
`return_type.size.value` unconditionally, and `bit` has no size. The
declared width is now evaluated by the caller and passed in, and a cast
failure is re-raised as `Return type mismatch for subroutine 'f'.
Expected bit[2] but got float`, naming both types instead of leaking an
`AttributeError` or a bare cast message.

A bit declaration is emitted verbatim while the subroutine definition is
not, so a `bit` return also left an unresolved `f()` call in the output.
The call is now replaced by the expression standing in for its return
value: a `BitstringLiteral` when the register is statically known, and a
reference when it is not. The substitution is applied to a copy of the
statement so the source AST keeps its call and the module can be
unrolled again -- `depth()` re-unrolls a copy.

`return measure q;` needed both halves. `_visit_measurement` never
applied the function qubit transform, so any measurement on a formal
argument raised "Missing register declaration". It now rewrites the
operand innermost scope outwards, exactly as `_visit_reset` does. The
measurement itself has no compile-time value, so it is bound to a
temporary bit register declared at the call site; the caller's variable
is then initialised from that register, which keeps the emitted program
valid OpenQASM and gives the measurement a classical target. Assign-
then-return returns the local bit by reference instead, and a nested
subroutine forwards whichever the inner call produced.

Closes #387

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 57fbc5ef-8db7-4df8-aa9c-e309f0a22969

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

`_visit_function_call` only shallow-copies each body statement, so the
inner `QuantumMeasurement` node stays shared with the subroutine
definition. Rewriting `statement.measure.qubit` in place therefore wrote
the caller's qubits onto the definition itself, and the next visit of
that body read a list where an `Identifier` was expected:

    AttributeError: 'list' object has no attribute 'name'

The shallow copy protects `statement.qubits` for `_visit_reset`, which
rebinds one level up; a measurement's operand sits one level deeper.

Reported as `validate()` followed by `unroll()` on an assign-then-return
subroutine, but the trigger is any second visit of a subroutine body
containing a measurement, with or without a return: calling such a
subroutine twice in a single `unroll()` failed the same way.

`_visit_measurement` now copies the statement and its `measure` node
before applying the transform. `return measure q;` was already immune
because `_synthesize_measurement_return` deep-copies the expression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@argus-eye

argus-eye Bot commented Aug 24, 2026

Copy link
Copy Markdown

Argus review

Auto-review is off for this repo. Tick the box below to run a review on this PR.

  • Trigger Argus review

Estimated cost

  • Files changed: 5
  • Diff lines (±): 689
  • Historical avg: ~243.6k tokens · ~$0.95 · across last 10 review(s)

Tip: you can also comment @argus-eye review at any time.

@TheGupta2012
TheGupta2012 marked this pull request as ready for review August 26, 2026 11:11
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

2 participants