Skip to content

Add C API for inspecting classical expressions - #16178

Merged
alexanderivrii merged 7 commits into
Qiskit:mainfrom
eliarbel:c-api-inspect-expr
Jun 14, 2026
Merged

Add C API for inspecting classical expressions#16178
alexanderivrii merged 7 commits into
Qiskit:mainfrom
eliarbel:c-api-inspect-expr

Conversation

@eliarbel

@eliarbel eliarbel commented May 14, 2026

Copy link
Copy Markdown
Member

This PR adds functions for inspecting classical expressions in the C API.

Details

This PR part of the work on #15980. Its goal is to support full inspection of control flow operations, which might
include conditions that rely on classical expressions.

The properties of expression nodes are extracted using accessor functions, e.g. for querying the kind of
an expression node and other information represented by new enums. In addition the PR adds dedicated structs
for representing various properties of the different node kinds. For example, the information of a binary expression
node is inspected using this struct:

pub struct CBinaryExprInfo {
    /// The binary operator
    pub op: CBinaryOpType,
    /// Borrowed pointer to the left operand expression
    pub left: *const Expr,
    /// Borrowed pointer to the right operand expression
    pub right: *const Expr,
    /// Result type of the operation
    pub ty: CExprTypeInfo,
    /// Whether the expression is constant
    pub constant: bool,
}

In its current form, the C API for inspecting classical expressions only provides borrowed pointers access to expression nodes. So the user should not
(and currently could not, since there is no qk_expr_free function) free QkExprNode objects while traversing a given expression. Consequently,
the API assumes that any expression being traversed remains alive and unchanged for the duration its nodes are being accessed.

For testing the API in C (in test_classical_expr.c), this PR also adds inner helper functions in Rust to generate various test expressions used in the C tests. These functions will be replaced by proper C API for creating classical expressions, once we have it (there is a WIP PR for next release #16180).

This PR is based on #16176 and will be rebased once it's merged.

AI/LLM disclosure

  • I used the following tool to generate or modify code:
    Bob 1.0.2 was used to generate initial versions of the C tests and documentation.

Tasks

  • Add functions to the v tables
  • Add C tests
    - [ ] Add Rust tests Rust test will be added to the PR for creating (and freeing) control flow expression objects.
  • Add documentation and reno

@eliarbel eliarbel added this to the 2.5.0 milestone May 14, 2026
@eliarbel eliarbel added Changelog: Added Add an "Added" entry in the GitHub Release changelog. C API Related to the C API labels May 14, 2026
@eliarbel eliarbel moved this from Ready to Blocked in Qiskit 2.5 May 14, 2026
@eliarbel eliarbel added the on hold Can not fix yet label May 14, 2026
@eliarbel
eliarbel force-pushed the c-api-inspect-expr branch from 7787241 to 9a5f312 Compare May 19, 2026 14:53
@coveralls

coveralls commented May 19, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 27089497458

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.08%) to 87.435%

Details

  • Coverage decreased (-0.08%) from the base build.
  • Patch coverage: 85 uncovered changes across 2 files (445 of 530 lines covered, 83.96%).
  • 3639 coverage regressions across 120 files.

Uncovered Changes

File Changed Covered %
crates/cext/src/classical_expr.rs 436 390 89.45%
crates/bindgen/src/render/rust.rs 39 0 0.0%
Total (4 files) 530 445 83.96%

Coverage Regressions

3639 previously-covered lines in 120 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
crates/circuit/src/dag_circuit.rs 503 85.27%
crates/qpy/src/circuit_reader.rs 213 77.34%
crates/bindgen-cli/src/abi.rs 210 0.0%
crates/circuit/src/circuit_data.rs 209 86.55%
crates/circuit/src/operations.rs 191 81.98%
crates/transpiler/src/target/mod.rs 157 81.96%
crates/cext/src/circuit.rs 121 79.58%
crates/qpy/src/value.rs 108 71.01%
crates/quantum_info/src/sparse_observable/mod.rs 83 93.37%
qiskit/qpy/binary_io/value.py 82 62.22%

Coverage Stats

Coverage Status
Relevant Lines: 125645
Covered Lines: 109858
Line Coverage: 87.44%
Coverage Strength: 955586.78 hits per line

💛 - Coveralls

@eliarbel
eliarbel force-pushed the c-api-inspect-expr branch 5 times, most recently from 30f576e to 8749d13 Compare May 26, 2026 05:12
@eliarbel
eliarbel marked this pull request as ready for review May 26, 2026 06:19
@eliarbel
eliarbel requested a review from a team as a code owner May 26, 2026 06:19
@eliarbel
eliarbel requested a review from alexanderivrii May 26, 2026 06:19
@qiskit-bot

Copy link
Copy Markdown
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@eliarbel

Copy link
Copy Markdown
Member Author

This is now ready for review. The changes added by this PR are all contained here: 8749d13

@ShellyGarion ShellyGarion moved this from Blocked to Ready in Qiskit 2.5 May 28, 2026
@alexanderivrii alexanderivrii self-assigned this May 29, 2026

@alexanderivrii alexanderivrii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Eli. The PR looks good overall, and the tests seem thorough. I have left a few questions inline.

Comment thread crates/cext/src/classical_expr.rs Outdated
Comment thread crates/cext/src/classical_expr.rs Outdated
Comment thread crates/cext/src/classical_expr.rs
Comment thread crates/cext/src/classical_expr.rs Outdated
Comment on lines +763 to +764
/// This function panics if ``value`` does not point to a ``QkExprType::Uint`` value or if the
/// stored integer does not fit in ``uint64_t``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am wondering if this is an assumption we can make. The Rust data structure uses BigUint and can store values that have more than 64 bits, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, good question, and also one of the design decisions I'd be happy to get feedback on.
If you come from Python, then yes, the circuit can contain conditions larger than uint64_t. But QPY doesn't support register conditions which are lager than i64::MAX. For example:

qc = QuantumCircuit(1, 1)

with qc.if_test((qc.cregs[0], 1 << 63)):
    qc.h(0)

with open("/tmp/qc.qpy", "wb") as f:
    dump(qc, f)

gives:

qiskit.qpy.exceptions.QpyError: 'invalid instruction: Register condition value 9223372036854775808 exceeds i64::MAX and cannot be serialized in QPY format'

So practically it seems like we're saying that 64 bits should be sufficient, but this is questionable, I agree. On the other hand, since we don't have big integer support currently in the C API, I thought that for now we can make this assumption and later, if true big int support will be required, we can add it with additional accessors like qk_value_big_uint or something. Let me know what you think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is definitely reasonable for now, and (as you said) we can add more access methods in the future. Two more minor questions about this. I don't think there is a way for the user to find the width of the Value, should we already add one, or this is a bit premature? Should we introduce instead the non-panicking variant qk_value_try_uint? As I said, I am completely fine with the current API, just throwing additional suggestions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sure, we can already output complete type information for Value. Changed in 81cc1b8.
This will allow users to check that the width does not exceed 64 bits before calling qk_value_uint.

Comment thread crates/cext/src/classical_expr.rs
Comment thread crates/cext/src/classical_expr.rs Outdated
Comment thread crates/cext/Cargo.toml
Comment on lines +11 to +26
* :c:func:`qk_expr_node_kind`
* :c:func:`qk_expr_binary_info`
* :c:func:`qk_expr_unary_info`
* :c:func:`qk_expr_cast_info`
* :c:func:`qk_expr_index_info`
* :c:func:`qk_expr_as_value`
* :c:func:`qk_expr_as_var`
* :c:func:`qk_expr_as_stretch`
* :c:func:`qk_value_type`
* :c:func:`qk_value_duration_info`
* :c:func:`qk_value_float`
* :c:func:`qk_value_uint`
* :c:func:`qk_value_bool`
* :c:func:`qk_var_name`
* :c:func:`qk_var_type_info`
* :c:func:`qk_stretch_name`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know that you have a documentation PR planned, but it would be nice to see more structure here, instead of "here is a list of all functions, look up the details by yourself".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm generally in the camp that release notes should be mostly a concise list of the changes with references to the documentation for more details. But that said, I'd be happy to improve this note. Do you have anything particular in mind? I think that any code example would have to be either very partial or very long, so possibly confusing? maybe we should add one line description for each function? Let me know what you think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree, we prefer not to add long examples to the release note. I like your previous suggestion of creating a separate rst file. In either case, this documentation will be included in a separate PR, correct?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. I plan to open a PR for it soon

int result = Ok;
QkExprNode *expr = NULL;

for (int op = QkUnaryOpType_BitNot; op <= QkUnaryOpType_Negate; op++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is exhaustive for now, but what if more unary types are added in the future, and QkUnaryOpType_Negate will not be the last one?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, this is an absolutely valid point, and TBH, I've already used this pattern before 😄

for (uint8_t gate = 0; gate <= QkGate_RC3X; ++gate) {

I don't know of a good solution for this which is not API-intrusive, like adding a marker field (say QkUnaryOpType_LAST) or using magic crates like strum.
Maybe we can go with a Rust test (more of a reminder) which "fires" in case the used enum is updated in Rust. Something like:

/// A compile-time reminder to ensure updates in CUnaryOpType are 
/// properly handled in the C API tests w.r.t the below: 
/// 
/// If this function doesn't compile, make sure to update the for-loops (e.g. in test_classical_expr.rs)
/// which explicitly iterate over the enum values.
#[test]
fn test_CUnaryOpType_stability() {
    let dummy = CUnaryOpType::BitNot;
    match dummy {
        CUnaryOpType::BitNot | CUnaryOpType::LogicNot | CUnaryOpType::Negate => {},

    };
}

?
If so I think it'd be a good follow-up PR (for handle all cases like this we currently have in the C API).

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.

Perhaps we should expose a constant that represents the max amount in the StandardGate enumeration in a separate PR

@alexanderivrii alexanderivrii Jun 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Eli and Ray. I am fine with leaving this test as it is right now, but longer-term I prefer a marker QkUnaryOpType_LAST rather a stability test (same for standard gates too).

Update: or looking into using the strum crate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thinking a bit more about this, I'm not sure using a marker is such a good idea. For example,
assume we use it for the StandardGate enum and at some point we add a new standard gate with more than 4 qubits and/or more than 4 params. While the marker will ensure that the test here:

qiskit/test/c/test_circuit.c

Lines 1167 to 1170 in 45d78c7

uint32_t qubits[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
double params[4] = {1.41, 2.71, 3.14, 1.5};
for (uint8_t gate = 0; gate <= QkGate_RC3X; ++gate) {
qk_circuit_gate(circuit, gate, &qubits[gate % 8], params);

will still compile (assuming we use the marker value in the for-loop), the test will fail measurably since the qubits and params arrays are initialized with certain upper limit assumptions re the max number of qubits/params for standard gates. So, I think that from that perspective a Rust-test with an explicit and clear reminder or TODO comment which fails if the Rust enum changes is more appropriate.

Comment thread test/c/test_classical_expr.c Outdated
@eliarbel
eliarbel requested a review from alexanderivrii June 7, 2026 10:17

@raynelfss raynelfss left a comment

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.

I took a glimpse at the code, overall it looks very intricate but such are classical expressions. There are pieces of the code that can be replaced to From<T> for U implementations.

Comment thread crates/cext/src/classical_expr.rs Outdated
Comment on lines +106 to +113
fn to_type(&self) -> Type {
match self.ty {
CExprType::Bool => Type::Bool,
CExprType::Duration => Type::Duration,
CExprType::Float => Type::Float,
CExprType::Uint => Type::Uint(self.width),
}
}

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 could be consolidated into impl From<CExprType> for Type

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah, cool! I avoided this a priori assuming that Rust wouldn't allow it because of the orphan rule, but apparantly I had a stricter version of the rule in my mind than it actually is.
Good catch, thanks! Done in facd4eb

Comment thread crates/cext/src/classical_expr.rs Outdated
Comment on lines +161 to +169
impl CUnaryOpType {
fn to_unary_op(&self) -> UnaryOp {
match self {
CUnaryOpType::BitNot => UnaryOp::BitNot,
CUnaryOpType::LogicNot => UnaryOp::LogicNot,
CUnaryOpType::Negate => UnaryOp::Negate,
}
}
}

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 could be reimplemented as impl From<CUnaryOpType> for UnaryOp

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. See #16178 (comment)

Comment thread crates/cext/src/classical_expr.rs Outdated
Comment on lines +251 to +271
fn to_binary_op(&self) -> BinaryOp {
match self {
CBinaryOpType::BitAnd => BinaryOp::BitAnd,
CBinaryOpType::BitOr => BinaryOp::BitOr,
CBinaryOpType::BitXor => BinaryOp::BitXor,
CBinaryOpType::LogicAnd => BinaryOp::LogicAnd,
CBinaryOpType::LogicOr => BinaryOp::LogicOr,
CBinaryOpType::Equal => BinaryOp::Equal,
CBinaryOpType::NotEqual => BinaryOp::NotEqual,
CBinaryOpType::Less => BinaryOp::Less,
CBinaryOpType::LessEqual => BinaryOp::LessEqual,
CBinaryOpType::Greater => BinaryOp::Greater,
CBinaryOpType::GreaterEqual => BinaryOp::GreaterEqual,
CBinaryOpType::ShiftLeft => BinaryOp::ShiftLeft,
CBinaryOpType::ShiftRight => BinaryOp::ShiftRight,
CBinaryOpType::Add => BinaryOp::Add,
CBinaryOpType::Sub => BinaryOp::Sub,
CBinaryOpType::Mul => BinaryOp::Mul,
CBinaryOpType::Div => BinaryOp::Div,
}
}

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.

impl From<CBinaryOpType> for BinaryOp

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. See #16178 (comment)

Comment thread crates/cext/src/classical_expr.rs Outdated
}

impl CDurationInfo {
pub fn to_duration(&self) -> Duration {

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 this conversion is unsafe, I'm not sure we should re-implement it with a From<T> impl here. But I still wanted to bring it up, in case it is a possibilty.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't see a reason why shouldn't do this, even if there are unsafe blocks in the converter. Changed in facd4eb

int result = Ok;
QkExprNode *expr = NULL;

for (int op = QkUnaryOpType_BitNot; op <= QkUnaryOpType_Negate; op++) {

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.

Perhaps we should expose a constant that represents the max amount in the StandardGate enumeration in a separate PR

@alexanderivrii alexanderivrii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Eli for the very detailed feedback. I am fine not including a more detailed C API documentation in this PR, but we should not forget doing this.

Can you rebase it on top of main and fix the merge conflict?

@alexanderivrii alexanderivrii removed the on hold Can not fix yet label Jun 12, 2026
@eliarbel
eliarbel force-pushed the c-api-inspect-expr branch from 8ddb376 to facd4eb Compare June 14, 2026 06:02
@eliarbel

Copy link
Copy Markdown
Member Author

Thanks Sasha and Ray for your reviews. I've rebased the PR and addressed your comments.

@eliarbel
eliarbel requested a review from alexanderivrii June 14, 2026 06:53

@alexanderivrii alexanderivrii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Eli. I am fine with the PR, and since you have addressed Ray's comments as well, for the interest of time I will approve it and merge.

@alexanderivrii
alexanderivrii added this pull request to the merge queue Jun 14, 2026
Merged via the queue into Qiskit:main with commit 112cf2b Jun 14, 2026
27 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in Qiskit 2.5 Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C API Related to the C API Changelog: Added Add an "Added" entry in the GitHub Release changelog.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants