Add C API for inspecting classical expressions - #16178
Conversation
7787241 to
9a5f312
Compare
Coverage Report for CI Build 27089497458Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage decreased (-0.08%) to 87.435%Details
Uncovered Changes
Coverage Regressions3639 previously-covered lines in 120 files lost coverage.
Coverage Stats
💛 - Coveralls |
30f576e to
8749d13
Compare
|
One or more of the following people are relevant to this code:
|
|
This is now ready for review. The changes added by this PR are all contained here: 8749d13 |
alexanderivrii
left a comment
There was a problem hiding this comment.
Thanks Eli. The PR looks good overall, and the tests seem thorough. I have left a few questions inline.
| /// This function panics if ``value`` does not point to a ``QkExprType::Uint`` value or if the | ||
| /// stored integer does not fit in ``uint64_t``. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| * :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` |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Yes, this is an absolutely valid point, and TBH, I've already used this pattern before 😄
Line 1169 in 0278105
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).
There was a problem hiding this comment.
Perhaps we should expose a constant that represents the max amount in the StandardGate enumeration in a separate PR
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
Lines 1167 to 1170 in 45d78c7
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.
raynelfss
left a comment
There was a problem hiding this comment.
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.
| 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), | ||
| } | ||
| } |
There was a problem hiding this comment.
This could be consolidated into impl From<CExprType> for Type
There was a problem hiding this comment.
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
| impl CUnaryOpType { | ||
| fn to_unary_op(&self) -> UnaryOp { | ||
| match self { | ||
| CUnaryOpType::BitNot => UnaryOp::BitNot, | ||
| CUnaryOpType::LogicNot => UnaryOp::LogicNot, | ||
| CUnaryOpType::Negate => UnaryOp::Negate, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This could be reimplemented as impl From<CUnaryOpType> for UnaryOp
| 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
impl From<CBinaryOpType> for BinaryOp
| } | ||
|
|
||
| impl CDurationInfo { | ||
| pub fn to_duration(&self) -> Duration { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
Perhaps we should expose a constant that represents the max amount in the StandardGate enumeration in a separate PR
alexanderivrii
left a comment
There was a problem hiding this comment.
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?
8ddb376 to
facd4eb
Compare
|
Thanks Sasha and Ray for your reviews. I've rebased the PR and addressed your comments. |
alexanderivrii
left a comment
There was a problem hiding this comment.
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.
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:
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_freefunction) freeQkExprNodeobjects 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
Bob 1.0.2 was used to generate initial versions of the C tests and documentation.
Tasks
- [ ] Add Rust testsRust test will be added to the PR for creating (and freeing) control flow expression objects.