Skip to content

feat: normalize behavior and usage of Instruction::get_qubits#497

Open
mingyoungjeng wants to merge 4 commits intomainfrom
normalize-get-qubits
Open

feat: normalize behavior and usage of Instruction::get_qubits#497
mingyoungjeng wants to merge 4 commits intomainfrom
normalize-get-qubits

Conversation

@mingyoungjeng
Copy link
Copy Markdown
Collaborator

@mingyoungjeng mingyoungjeng commented Feb 25, 2026

  • get_qubits and get_qubits_mut now properly returns the qubits used for previously-unaccounted-for Instructions
  • Program::add_instruction only caches body instructions in Program::used_qubits.
  • Generalizes Instruction::get_qubits and Instruction::get_qubits_mut functionality into the InstructionHandler trait

Closes #498

@mingyoungjeng mingyoungjeng self-assigned this Feb 25, 2026
Copy link
Copy Markdown

@windsurf-bot windsurf-bot bot left a comment

Choose a reason for hiding this comment

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

Other comments (7)
  • quil-rs/src/instruction/mod.rs (824-824) Same syntax error with double colon instead of single colon for generic type parameter.
                            .flat_map(|inst| inst.default_qubits_mut::<T>()),
    
  • quil-rs/src/instruction/mod.rs (727-727) Same syntax error with double colon instead of single colon for generic type parameter.
                            .flat_map(|inst| inst.default_qubits::<T>()),
    
  • quil-rs/src/instruction/mod.rs (835-835) Same syntax error with double colon instead of single colon for generic type parameter.
                            .flat_map(|inst| inst.default_qubits_mut::<T>()),
    
  • quil-rs/src/instruction/mod.rs (949-949) Same syntax error with double colon instead of single colon for generic type parameter.
                    for qubit in other.default_qubits_mut::<Vec<_>>() {
    
  • quil-rs/src/parser/command.rs (904-911) There's an inconsistency between the implementation and test expectations. The PR changes `CircuitDefinition` to use a constructor method and replaces `qubit_variables` with `qubits`, but the test cases still expect the old field name `qubit_variables`. This will likely cause test failures.
  • quil-rs/src/program/mod.rs (435-435) The `for_each_body_instruction` method was previously marked as `#[cfg(test)]` but is now public. Was this intentional? If so, consider adding documentation to explain its purpose and usage for public API consumers.
  • quil-rs/src/program/mod.rs (266-270) This change modifies when qubits are added to `used_qubits`. Previously, all instructions would have their qubits tracked, but now only body instructions do. This is a behavior change that might affect users who rely on the previous behavior. Consider documenting this change in the PR description or adding a comment explaining the rationale.

💡 To request another review, post a new comment with "/windsurf-review".

Comment thread quil-rs/src/instruction/mod.rs
Comment thread quil-rs/src/instruction/mod.rs
Comment thread quil-rs/src/instruction/mod.rs
Copy link
Copy Markdown
Contributor

@asaites asaites left a comment

Choose a reason for hiding this comment

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

Overall I think this is on the right track, but needs a little adjustment. I think there's an inconsistency in the qubit caching when adding an instruction vs rebuilding the cache directly.

Comment thread quil-rs/src/instruction/circuit.rs Outdated
@@ -1,8 +1,11 @@
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You've got a compilation error because this needs to be:

Suggested change
use pyo3_stub_gen::derive::gen_stub_pyclass;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};

You can test these ahead of time by running

cargo make test-all

Along the same lines, you need to regenerate the type stubs and check them, since you've made changes to the Python API. You can do that with:

cargo make generate-stubs

Since that requires the stubs feature, in this instance, it would have caught the issue.

#[cfg_attr(not(feature = "python"), strip_pyo3)]
impl CircuitDefinition {
#[new]
pub fn new(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since you're not able to use pickleable_new here1, you should implement __getnewargs__ manually so that CircuitDefinitions (and its subclasses) remain pickleable.

Since doing so is purely for Python users, our convention is to put it in a quilpy module. In this case, the appropriate place is src/instructions/quilpy.rs, likely around line 460.

Here's one possible implementation (the important thing is that its arguments are compatible with the method marked #[new]):

#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
#[pymethods]
impl CircuitDefinition {
    fn __getnewargs__<'py>(&self, py: pyo3::Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
        (
            self.name.clone(),
            self.parameters.clone(),
            self.qubits.clone(),
            self.instructions.clone(),
        ).into_pyobject(py)
    }
}

Footnotes

  1. I'd still like to look into improving this macro to make it useful across a wider range of cases, and likely move it to rigetti-pyo3.

Comment thread quil-rs/src/instruction/circuit.rs Outdated

// These cannot be fixed qubits and thus cannot be accessed directly
// outside of this crate
#[pyo3(get, set)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You probably don't want set, if these must be Qubit::Variable, as otherwise Python users can set the list to other kinds of Qubits.

To that end, I think you should at least document that this must be Qubit::Variable explicitly. You could enforce that with a newtype that wraps String and serves as the type of both Qubit::Variable and the type of this Vec, but its not clear to me that's any better than just using String itself, which you're trying to change. That leads me to the obvious question:

Why should this type be Vec<Qubit> if it really is restricted to only the Variable variant?

Copy link
Copy Markdown
Collaborator Author

@mingyoungjeng mingyoungjeng Mar 3, 2026

Choose a reason for hiding this comment

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

The main reason I had to make it Vec<Qubit> instead of the prior Vec<String> is because I needed to extract &Qubits from the circuit definition.

I could try to make something like this work?

enum QubitRef<'a> {
    Fixed(u64),
    Placeholder(QubitPlaceholder),
    Variable(&str),
}

}

#[cfg(feature = "python")]
impl<'a, T: AsRef<str>> GateSignature<'a, T> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I think you should just pull the try_new method into a separate impl block instead of repeating the #[cfg(feature = "python")]s.

/// Return immutable references to the [`Qubit`]s contained within an instruction
#[allow(dead_code)]
pub fn get_qubits(&self) -> Vec<&Qubit> {
pub(crate) fn default_qubits<'a, T>(&'a self) -> T
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As I look at this implementation, I am starting to think we should have an actual QubitIterator that can encapsulate this logic without all the collects. I'd like to see some performance details on this, as perhaps it wouldn't matter anyway, but at the very least, the implementation gets pretty messy here. I could imagine it being more of a performance/usability improvement for the Python API, but again, we'd need some profiling data to really say.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Alternatively, I could just force it to be HashSet or Vec. It was Vec previously, but it makes more sense (imo) to be HashSet -- all the consumers either didn't care or would have preferred HashSet.

.to_instructions()
.iter()
.flat_map(|instruction| instruction.get_qubits().into_iter().cloned())
.flat_map(|instruction| DefaultHandler.get_qubits(instruction).into_iter().cloned())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can result in the same used_qubits as the old add_instruction implementation: if you call resolve_placeholders_with_custom_resolvers() (which calls this method) on a Program with DEFCAL instructions, the Variables end up in the cache. That was also true before this PR, but it represents an inconsistency between the way add_instruction and this method work within this PR.

I'll also note that DEFCIRCUIT qubits will also make their way into the cache after calling this method, but that wasn't true before the PR.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://rigetti.github.io/quil-rs/pr-preview/pr-497/

Built to branch quil-py-docs at 2026-03-04 18:12 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

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.

The qubits "used" by instructions and programs aren't calculated correctly

2 participants