• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 13817569079

12 Mar 2025 04:49PM UTC coverage: 88.12% (-0.004%) from 88.124%
13817569079

push

github

web-flow
Fix string and standard gate mismatch in commutation checker (backport #13991) (#14004)

* Fix string and standard gate mismatch in commutation checker (#13991)

* Fix string and standard gate mismatch in commutation checker

This commit reworks the internals of the CommutationChecker to not rely
on operation name except for where we do a lookup by name in the
commutation library provided (which is the only key available to support
custom gates). This fixes the case where a custom gate that overloads
the standard gate name, previously the code would assume it to be a
standard gate and internally panic when it wasn't. When working with
standard gates (or standard instructions) we don't need to rely on
string matching because we can rely on the rust data model to do the
heavy lifting for us. This commit moves all the explicit handling of
standard gates to use the StandardGate type directly and makes this
logic more robust.

This also removes are usage of the once_cell library in
qiskit-accelerate because it was used to create a lazy static hashsets
of strings which are no longer needed because static lookup tables replace
this when we stopped using string comparisons.

Fixes #13988

* Rename is_commutation_skipped() is_parameterized()

(cherry picked from commit abb0cf9db)

# Conflicts:
#	crates/accelerate/src/commutation_checker.rs

* Update commutation_checker.rs

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>

47 of 91 new or added lines in 1 file covered. (51.65%)

8 existing lines in 3 files now uncovered.

72643 of 82436 relevant lines covered (88.12%)

499533.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.23
/crates/qasm2/src/expr.rs
1
// This code is part of Qiskit.
2
//
3
// (C) Copyright IBM 2023
4
//
5
// This code is licensed under the Apache License, Version 2.0. You may
6
// obtain a copy of this license in the LICENSE.txt file in the root directory
7
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
//
9
// Any modifications or derivative works of this code must retain this
10
// copyright notice, and modified files need to carry a notice indicating
11
// that they have been altered from the originals.
12

13
//! An operator-precedence subparser used by the main parser for handling parameter expressions.
14
//! Instances of this subparser are intended to only live for as long as it takes to parse a single
15
//! parameter.
16

17
use core::f64;
18

19
use hashbrown::HashMap;
20
use pyo3::prelude::*;
21
use pyo3::types::PyTuple;
22

23
use crate::bytecode;
24
use crate::error::{
25
    message_bad_eof, message_generic, message_incorrect_requirement, Position, QASM2ParseError,
26
};
27
use crate::lex::{Token, TokenContext, TokenStream, TokenType};
28
use crate::parse::{GateSymbol, GlobalSymbol, ParamId};
29

30
/// Enum representation of the builtin OpenQASM 2 functions.  The built-in Qiskit parser adds the
31
/// inverse trigonometric functions, but these are an extension to the version as given in the
32
/// arXiv paper describing OpenQASM 2.  This enum is essentially just a subset of the [TokenType]
33
/// enum, to allow for better pattern-match checking in the Rust compiler.
34
pub enum Function {
35
    Cos,
36
    Exp,
37
    Ln,
38
    Sin,
39
    Sqrt,
40
    Tan,
41
}
42

43
impl From<TokenType> for Function {
44
    fn from(value: TokenType) -> Self {
116✔
45
        match value {
116✔
46
            TokenType::Cos => Function::Cos,
22✔
47
            TokenType::Exp => Function::Exp,
12✔
48
            TokenType::Ln => Function::Ln,
30✔
49
            TokenType::Sin => Function::Sin,
20✔
50
            TokenType::Sqrt => Function::Sqrt,
20✔
51
            TokenType::Tan => Function::Tan,
12✔
52
            _ => panic!(),
×
53
        }
54
    }
116✔
55
}
56

57
impl From<Function> for bytecode::UnaryOpCode {
58
    fn from(value: Function) -> Self {
40✔
59
        match value {
40✔
60
            Function::Cos => Self::Cos,
10✔
61
            Function::Exp => Self::Exp,
6✔
62
            Function::Ln => Self::Ln,
6✔
63
            Function::Sin => Self::Sin,
6✔
64
            Function::Sqrt => Self::Sqrt,
6✔
65
            Function::Tan => Self::Tan,
6✔
66
        }
67
    }
40✔
68
}
69

70
/// An operator symbol used in the expression parsing.  This is essentially just a subset of the
71
/// [TokenType] enum (albeit with resolved names) to allow for better pattern-match semantics in
72
/// the Rust compiler.
73
#[derive(Clone, Copy)]
74
enum Op {
75
    Plus,
76
    Minus,
77
    Multiply,
78
    Divide,
79
    Power,
80
}
81

82
impl Op {
83
    fn text(&self) -> &'static str {
6✔
84
        match self {
6✔
85
            Self::Plus => "+",
×
86
            Self::Minus => "-",
×
87
            Self::Multiply => "*",
2✔
88
            Self::Divide => "/",
2✔
89
            Self::Power => "^",
2✔
90
        }
91
    }
6✔
92
}
93

94
impl From<TokenType> for Op {
95
    fn from(value: TokenType) -> Self {
688✔
96
        match value {
688✔
97
            TokenType::Plus => Op::Plus,
136✔
98
            TokenType::Minus => Op::Minus,
236✔
99
            TokenType::Asterisk => Op::Multiply,
82✔
100
            TokenType::Slash => Op::Divide,
204✔
101
            TokenType::Caret => Op::Power,
30✔
102
            _ => panic!(),
×
103
        }
104
    }
688✔
105
}
106

107
/// An atom of the operator-precedence expression parsing.  This is a stripped-down version of the
108
/// [Token] and [TokenType] used in the main parser.  We can use a data enum here because we do not
109
/// need all the expressive flexibility in expecting and accepting many different token types as
110
/// we do in the main parser; it does not significantly harm legibility to simply do
111
///
112
/// ```rust
113
/// match atom {
114
///     Atom::Const(val) => (),
115
///     Atom::Parameter(index) => (),
116
///     // ...
117
/// }
118
/// ```
119
///
120
/// where required.
121
enum Atom {
122
    LParen,
123
    RParen,
124
    Function(Function),
125
    CustomFunction(PyObject, usize),
126
    Op(Op),
127
    Const(f64),
128
    Parameter(ParamId),
129
}
130

131
/// A tree representation of parameter expressions in OpenQASM 2.  The expression
132
/// operator-precedence parser will do complete constant folding on operations that only involve
133
/// floating-point numbers, so these will simply be evaluated into a `Constant` variant rather than
134
/// represented in full tree form.  For references to the gate parameters, we just store the index
135
/// of which parameter it is.
136
pub enum Expr {
137
    Constant(f64),
138
    Parameter(ParamId),
139
    Negate(Box<Expr>),
140
    Add(Box<Expr>, Box<Expr>),
141
    Subtract(Box<Expr>, Box<Expr>),
142
    Multiply(Box<Expr>, Box<Expr>),
143
    Divide(Box<Expr>, Box<Expr>),
144
    Power(Box<Expr>, Box<Expr>),
145
    Function(Function, Box<Expr>),
146
    CustomFunction(PyObject, Vec<Expr>),
147
}
148

149
impl<'py> IntoPyObject<'py> for Expr {
150
    type Target = PyAny; // the Python type
151
    type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound`
152
    type Error = PyErr;
153

154
    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
410✔
155
        Ok(match self {
410✔
156
            Expr::Constant(value) => bytecode::ExprConstant { value }
126✔
157
                .into_pyobject(py)?
126✔
158
                .into_any(),
126✔
159
            Expr::Parameter(index) => bytecode::ExprArgument { index }
162✔
160
                .into_pyobject(py)?
162✔
161
                .into_any(),
162✔
162
            Expr::Negate(expr) => bytecode::ExprUnary {
8✔
163
                opcode: bytecode::UnaryOpCode::Negate,
8✔
164
                argument: expr.into_pyobject(py)?.unbind(),
8✔
165
            }
8✔
166
            .into_pyobject(py)?
8✔
167
            .into_any(),
8✔
168
            Expr::Add(left, right) => bytecode::ExprBinary {
18✔
169
                opcode: bytecode::BinaryOpCode::Add,
18✔
170
                left: left.into_pyobject(py)?.unbind(),
18✔
171
                right: right.into_pyobject(py)?.unbind(),
18✔
172
            }
18✔
173
            .into_pyobject(py)?
18✔
174
            .into_any(),
18✔
175
            Expr::Subtract(left, right) => bytecode::ExprBinary {
8✔
176
                opcode: bytecode::BinaryOpCode::Subtract,
8✔
177
                left: left.into_pyobject(py)?.unbind(),
8✔
178
                right: right.into_pyobject(py)?.unbind(),
8✔
179
            }
8✔
180
            .into_pyobject(py)?
8✔
181
            .into_any(),
8✔
182
            Expr::Multiply(left, right) => bytecode::ExprBinary {
14✔
183
                opcode: bytecode::BinaryOpCode::Multiply,
14✔
184
                left: left.into_pyobject(py)?.unbind(),
14✔
185
                right: right.into_pyobject(py)?.unbind(),
14✔
186
            }
14✔
187
            .into_pyobject(py)?
14✔
188
            .into_any(),
14✔
189
            Expr::Divide(left, right) => bytecode::ExprBinary {
16✔
190
                opcode: bytecode::BinaryOpCode::Divide,
16✔
191
                left: left.into_pyobject(py)?.unbind(),
16✔
192
                right: right.into_pyobject(py)?.unbind(),
16✔
193
            }
16✔
194
            .into_pyobject(py)?
16✔
195
            .into_any(),
16✔
196
            Expr::Power(left, right) => bytecode::ExprBinary {
6✔
197
                opcode: bytecode::BinaryOpCode::Power,
6✔
198
                left: left.into_pyobject(py)?.unbind(),
6✔
199
                right: right.into_pyobject(py)?.unbind(),
6✔
200
            }
6✔
201
            .into_pyobject(py)?
6✔
202
            .into_any(),
6✔
203
            Expr::Function(func, expr) => bytecode::ExprUnary {
40✔
204
                opcode: func.into(),
40✔
205
                argument: expr.into_pyobject(py)?.unbind(),
40✔
206
            }
40✔
207
            .into_pyobject(py)?
40✔
208
            .into_any(),
40✔
209
            Expr::CustomFunction(func, exprs) => bytecode::ExprCustom {
12✔
210
                callable: func,
12✔
211
                arguments: exprs
12✔
212
                    .into_iter()
12✔
213
                    .map(|expr| expr.into_pyobject(py).unwrap().unbind())
18✔
214
                    .collect(),
12✔
215
            }
12✔
216
            .into_pyobject(py)?
12✔
217
            .into_any(),
12✔
218
        })
219
    }
410✔
220
}
221

222
/// Calculate the binding power of an [Op] when used in a prefix position.  Returns [None] if the
223
/// operation cannot be used in the prefix position.  The binding power is on the same scale as
224
/// those returned by [binary_power].
225
fn prefix_power(op: Op) -> Option<u8> {
200✔
226
    match op {
200✔
227
        Op::Plus | Op::Minus => Some(5),
194✔
228
        _ => None,
6✔
229
    }
230
}
200✔
231

232
/// Calculate the binding power of an [Op] when used in an infix position.  The differences between
233
/// left- and right-binding powers represent the associativity of the operation.
234
fn binary_power(op: Op) -> (u8, u8) {
488✔
235
    // For new binding powers, use the odd number as the "base" and the even number one larger than
488✔
236
    // it to represent the associativity.  Left-associative operators bind more strongly to the
488✔
237
    // operand on their right (i.e. in `a + b + c`, the first `+` binds to the `b` more tightly
488✔
238
    // than the second, so we get the left-associative form), and right-associative operators bind
488✔
239
    // more strongly to the operand of their left.  The separation of using the odd--even pair is
488✔
240
    // so there's no clash between different operator levels, even accounting for the associativity
488✔
241
    // distinction.
488✔
242
    //
488✔
243
    // All powers should be greater than zero; we need zero free to be the base case in the
488✔
244
    // entry-point to the precedence parser.
488✔
245
    match op {
488✔
246
        Op::Plus | Op::Minus => (1, 2),
178✔
247
        Op::Multiply | Op::Divide => (3, 4),
282✔
248
        Op::Power => (8, 7),
28✔
249
    }
250
}
488✔
251

252
/// A subparser used to do the operator-precedence part of the parsing for individual parameter
253
/// expressions.  The main parser creates a new instance of this struct for each expression it
254
/// expects, and the instance lives only as long as is required to parse that expression, because
255
/// it takes temporary responsibility for the [TokenStream] that backs the main parser.
256
pub struct ExprParser<'a> {
257
    pub tokens: &'a mut Vec<TokenStream>,
258
    pub context: &'a mut TokenContext,
259
    pub gate_symbols: &'a HashMap<String, GateSymbol>,
260
    pub global_symbols: &'a HashMap<String, GlobalSymbol>,
261
    pub strict: bool,
262
}
263

264
impl ExprParser<'_> {
265
    /// Get the next token available in the stack of token streams, popping and removing any
266
    /// complete streams, except the base case.  Will only return `None` once all streams are
267
    /// exhausted.
268
    fn next_token(&mut self) -> PyResult<Option<Token>> {
3,326✔
269
        let mut pointer = self.tokens.len() - 1;
3,326✔
270
        while pointer > 1 {
3,326✔
271
            let out = self.tokens[pointer].next(self.context)?;
×
272
            if out.is_some() {
×
273
                return Ok(out);
×
274
            }
×
275
            self.tokens.pop();
×
276
            pointer -= 1;
×
277
        }
278
        self.tokens[0].next(self.context)
3,326✔
279
    }
3,326✔
280

281
    /// Peek the next token in the stack of token streams.  This does not remove any complete
282
    /// streams yet.  Will only return `None` once all streams are exhausted.
283
    fn peek_token(&mut self) -> PyResult<Option<&Token>> {
3,046✔
284
        let mut pointer = self.tokens.len() - 1;
3,046✔
285
        while pointer > 1 && self.tokens[pointer].peek(self.context)?.is_none() {
3,046✔
286
            pointer -= 1;
×
287
        }
×
288
        self.tokens[pointer].peek(self.context)
3,046✔
289
    }
3,046✔
290

291
    /// Get the filename associated with the currently active token stream.
292
    fn current_filename(&self) -> &std::ffi::OsStr {
88✔
293
        &self.tokens[self.tokens.len() - 1].filename
88✔
294
    }
88✔
295

296
    /// Expect a token of the correct [TokenType].  This is a direct analogue of
297
    /// [parse::State::expect].  The error variant of the result contains a suitable error message
298
    /// if the expectation is violated.
299
    fn expect(&mut self, expected: TokenType, required: &str, cause: &Token) -> PyResult<Token> {
358✔
300
        let token = match self.next_token()? {
358✔
301
            None => {
302
                return Err(QASM2ParseError::new_err(message_bad_eof(
×
303
                    Some(&Position::new(
×
304
                        self.current_filename(),
×
305
                        cause.line,
×
306
                        cause.col,
×
307
                    )),
×
308
                    required,
×
309
                )))
×
310
            }
311
            Some(token) => token,
358✔
312
        };
358✔
313
        if token.ttype == expected {
358✔
314
            Ok(token)
356✔
315
        } else {
316
            Err(QASM2ParseError::new_err(message_incorrect_requirement(
2✔
317
                required,
2✔
318
                &token,
2✔
319
                self.current_filename(),
2✔
320
            )))
2✔
321
        }
322
    }
358✔
323

324
    /// Peek the next token from the stream, and consume and return it only if it has the correct
325
    /// type.
326
    fn accept(&mut self, acceptable: TokenType) -> PyResult<Option<Token>> {
162✔
327
        match self.peek_token()? {
162✔
328
            Some(Token { ttype, .. }) if *ttype == acceptable => self.next_token(),
162✔
329
            _ => Ok(None),
140✔
330
        }
331
    }
162✔
332

333
    /// Apply a prefix [Op] to the current [expression][Expr].  If the current expression is a
334
    /// constant floating-point value the application will be eagerly constant-folded, otherwise
335
    /// the resulting [Expr] will have a tree structure.
336
    fn apply_prefix(&mut self, prefix: Op, expr: Expr) -> PyResult<Expr> {
194✔
337
        match prefix {
194✔
338
            Op::Plus => Ok(expr),
18✔
339
            Op::Minus => match expr {
176✔
340
                Expr::Constant(val) => Ok(Expr::Constant(-val)),
168✔
341
                _ => Ok(Expr::Negate(Box::new(expr))),
8✔
342
            },
343
            _ => panic!(),
×
344
        }
345
    }
194✔
346

347
    /// Apply a binary infix [Op] to the current [expression][Expr].  If both operands have
348
    /// constant floating-point values the application will be eagerly constant-folded, otherwise
349
    /// the resulting [Expr] will have a tree structure.
350
    fn apply_infix(&mut self, infix: Op, lhs: Expr, rhs: Expr, op_token: &Token) -> PyResult<Expr> {
388✔
351
        if let (Expr::Constant(val), Op::Divide) = (&rhs, infix) {
388✔
352
            if *val == 0.0 {
186✔
353
                return Err(QASM2ParseError::new_err(message_generic(
8✔
354
                    Some(&Position::new(
8✔
355
                        self.current_filename(),
8✔
356
                        op_token.line,
8✔
357
                        op_token.col,
8✔
358
                    )),
8✔
359
                    "cannot divide by zero",
8✔
360
                )));
8✔
361
            }
178✔
362
        };
202✔
363
        if let (Expr::Constant(val_l), Expr::Constant(val_r)) = (&lhs, &rhs) {
380✔
364
            // Eagerly constant-fold if possible.
365
            match infix {
318✔
366
                Op::Plus => Ok(Expr::Constant(val_l + val_r)),
48✔
367
                Op::Minus => Ok(Expr::Constant(val_l - val_r)),
34✔
368
                Op::Multiply => Ok(Expr::Constant(val_l * val_r)),
50✔
369
                Op::Divide => Ok(Expr::Constant(val_l / val_r)),
168✔
370
                Op::Power => Ok(Expr::Constant(val_l.powf(*val_r))),
18✔
371
            }
372
        } else {
373
            // If not, we have to build a tree.
374
            let id_l = Box::new(lhs);
62✔
375
            let id_r = Box::new(rhs);
62✔
376
            match infix {
62✔
377
                Op::Plus => Ok(Expr::Add(id_l, id_r)),
18✔
378
                Op::Minus => Ok(Expr::Subtract(id_l, id_r)),
8✔
379
                Op::Multiply => Ok(Expr::Multiply(id_l, id_r)),
14✔
380
                Op::Divide => Ok(Expr::Divide(id_l, id_r)),
16✔
381
                Op::Power => Ok(Expr::Power(id_l, id_r)),
6✔
382
            }
383
        }
384
    }
388✔
385

386
    /// Apply a "scientific calculator" built-in function to an [expression][Expr].  If the operand
387
    /// is a constant, the function will be constant-folded to produce a new constant expression,
388
    /// otherwise a tree-form [Expr] is returned.
389
    fn apply_function(&mut self, func: Function, expr: Expr, token: &Token) -> PyResult<Expr> {
108✔
390
        match expr {
108✔
391
            Expr::Constant(val) => match func {
68✔
392
                Function::Cos => Ok(Expr::Constant(val.cos())),
8✔
393
                Function::Exp => Ok(Expr::Constant(val.exp())),
6✔
394
                Function::Ln => {
395
                    if val > 0.0 {
24✔
396
                        Ok(Expr::Constant(val.ln()))
8✔
397
                    } else {
398
                        Err(QASM2ParseError::new_err(message_generic(
16✔
399
                            Some(&Position::new(
16✔
400
                                self.current_filename(),
16✔
401
                                token.line,
16✔
402
                                token.col,
16✔
403
                            )),
16✔
404
                            &format!(
16✔
405
                                "failure in constant folding: cannot take ln of non-positive {}",
16✔
406
                                val
16✔
407
                            ),
16✔
408
                        )))
16✔
409
                    }
410
                }
411
                Function::Sin => Ok(Expr::Constant(val.sin())),
10✔
412
                Function::Sqrt => {
413
                    if val >= 0.0 {
14✔
414
                        Ok(Expr::Constant(val.sqrt()))
6✔
415
                    } else {
416
                        Err(QASM2ParseError::new_err(message_generic(
8✔
417
                            Some(&Position::new(
8✔
418
                                self.current_filename(),
8✔
419
                                token.line,
8✔
420
                                token.col,
8✔
421
                            )),
8✔
422
                            &format!(
8✔
423
                                "failure in constant folding: cannot take sqrt of negative {}",
8✔
424
                                val
8✔
425
                            ),
8✔
426
                        )))
8✔
427
                    }
428
                }
429
                Function::Tan => Ok(Expr::Constant(val.tan())),
6✔
430
            },
431
            _ => Ok(Expr::Function(func, Box::new(expr))),
40✔
432
        }
433
    }
108✔
434

435
    fn apply_custom_function(
44✔
436
        &mut self,
44✔
437
        callable: PyObject,
44✔
438
        exprs: Vec<Expr>,
44✔
439
        token: &Token,
44✔
440
    ) -> PyResult<Expr> {
44✔
441
        if exprs.iter().all(|x| matches!(x, Expr::Constant(_))) {
44✔
442
            // We can still do constant folding with custom user classical functions, we're just
443
            // going to have to acquire the GIL and call the Python object the user gave us right
444
            // now.  We need to explicitly handle any exceptions that might occur from that.
445
            Python::with_gil(|py| {
32✔
446
                let args = PyTuple::new(
32✔
447
                    py,
32✔
448
                    exprs.iter().map(|x| {
32✔
449
                        if let Expr::Constant(val) = x {
22✔
450
                            *val
22✔
451
                        } else {
452
                            unreachable!()
×
453
                        }
454
                    }),
32✔
455
                )?;
32✔
456
                match callable.call1(py, args) {
32✔
457
                    Ok(retval) => {
30✔
458
                        match retval.extract::<f64>(py) {
30✔
459
                            Ok(fval) => Ok(Expr::Constant(fval)),
28✔
460
                            Err(inner) => {
2✔
461
                                let error = QASM2ParseError::new_err(message_generic(
2✔
462
                                Some(&Position::new(self.current_filename(), token.line, token.col)),
2✔
463
                                "user-defined function returned non-float during constant folding",
2✔
464
                            ));
2✔
465
                                error.set_cause(py, Some(inner));
2✔
466
                                Err(error)
2✔
467
                            }
468
                        }
469
                    }
470
                    Err(inner) => {
2✔
471
                        let error = QASM2ParseError::new_err(message_generic(
2✔
472
                            Some(&Position::new(
2✔
473
                                self.current_filename(),
2✔
474
                                token.line,
2✔
475
                                token.col,
2✔
476
                            )),
2✔
477
                            "caught exception when constant folding with user-defined function",
2✔
478
                        ));
2✔
479
                        error.set_cause(py, Some(inner));
2✔
480
                        Err(error)
2✔
481
                    }
482
                }
483
            })
32✔
484
        } else {
485
            Ok(Expr::CustomFunction(callable, exprs))
12✔
486
        }
487
    }
44✔
488

489
    /// If in `strict` mode, and we have a trailing comma, emit a suitable error message.
490
    fn check_trailing_comma(&self, comma: Option<&Token>) -> PyResult<()> {
166✔
491
        match (self.strict, comma) {
166✔
492
            (true, Some(token)) => Err(QASM2ParseError::new_err(message_generic(
2✔
493
                Some(&Position::new(
2✔
494
                    self.current_filename(),
2✔
495
                    token.line,
2✔
496
                    token.col,
2✔
497
                )),
2✔
498
                "[strict] trailing commas in parameter and qubit lists are forbidden",
2✔
499
            ))),
2✔
500
            _ => Ok(()),
164✔
501
        }
502
    }
166✔
503

504
    /// Convert the given general [Token] into the expression-parser-specific [Atom], if possible.
505
    /// Not all [Token]s have a corresponding [Atom]; if this is the case, the return value is
506
    /// `Ok(None)`.  The error variant is returned if the next token is grammatically valid, but
507
    /// not semantically, such as an identifier for a value of an incorrect type.
508
    fn try_atom_from_token(&self, token: &Token) -> PyResult<Option<Atom>> {
5,404✔
509
        match token.ttype {
5,404✔
510
            TokenType::LParen => Ok(Some(Atom::LParen)),
36✔
511
            TokenType::RParen => Ok(Some(Atom::RParen)),
1,288✔
512
            TokenType::Minus
513
            | TokenType::Plus
514
            | TokenType::Asterisk
515
            | TokenType::Slash
516
            | TokenType::Caret => Ok(Some(Atom::Op(token.ttype.into()))),
688✔
517
            TokenType::Cos
518
            | TokenType::Exp
519
            | TokenType::Ln
520
            | TokenType::Sin
521
            | TokenType::Sqrt
522
            | TokenType::Tan => Ok(Some(Atom::Function(token.ttype.into()))),
116✔
523
            // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0
524
            // integers can be interpreted as floats, and doing that allows us to gracefully handle
525
            // cases where a huge float would overflow a `usize`.  Never mind that in such a case,
526
            // there's almost certainly precision loss from the floating-point representing
527
            // having insufficient mantissa digits to faithfully represent the angle mod 2pi;
528
            // that's not our fault in the parser.
529
            TokenType::Real | TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))),
1,688✔
530
            TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))),
256✔
531
            TokenType::Id => {
532
                let id = token.text(self.context);
258✔
533
                match self.gate_symbols.get(id) {
258✔
534
                    Some(GateSymbol::Parameter { index }) => Ok(Some(Atom::Parameter(*index))),
188✔
535
                    Some(GateSymbol::Qubit { .. }) => {
536
                        Err(QASM2ParseError::new_err(message_generic(
2✔
537
                            Some(&Position::new(self.current_filename(), token.line, token.col)),
2✔
538
                            &format!("'{}' is a gate qubit, not a parameter", id),
2✔
539
                        )))
2✔
540
                    }
541
                    None => {
542
                        match self.global_symbols.get(id) {
68✔
543
                            Some(GlobalSymbol::Classical { callable, num_params }) => {
64✔
544
                                Ok(Some(Atom::CustomFunction(callable.clone(), *num_params)))
64✔
545
                            }
546
                            _ =>  {
547
                            Err(QASM2ParseError::new_err(message_generic(
4✔
548
                            Some(&Position::new(self.current_filename(), token.line, token.col)),
4✔
549
                            &format!(
4✔
550
                                "'{}' is not a parameter or custom instruction defined in this scope",
4✔
551
                                id,
4✔
552
                            ))))
4✔
553
                            }
554
                    }
555
                }
556
            }
557
            }
558
            _ => Ok(None),
1,074✔
559
        }
560
    }
5,404✔
561

562
    /// Peek at the next [Atom] (and backing [Token]) if the next token exists and can be converted
563
    /// into a valid [Atom].  If it can't, or if we are at the end of the input, the `None` variant
564
    /// is returned.
565
    fn peek_atom(&mut self) -> PyResult<Option<(Atom, Token)>> {
2,884✔
566
        if let Some(&token) = self.peek_token()? {
2,884✔
567
            if let Ok(Some(atom)) = self.try_atom_from_token(&token) {
2,884✔
568
                Ok(Some((atom, token)))
1,820✔
569
            } else {
570
                Ok(None)
1,064✔
571
            }
572
        } else {
573
            Ok(None)
×
574
        }
575
    }
2,884✔
576

577
    /// The main recursive worker routing of the operator-precedence parser.  This evaluates a
578
    /// series of binary infix operators that have binding powers greater than the input
579
    /// `power_min`, and unary prefixes on the left-hand operand.  For example, if `power_min`
580
    /// starts out at `2` (such as it would when evaluating the right-hand side of a binary `+`
581
    /// expression), then as many `*` and `^` operations as appear would be evaluated by this loop,
582
    /// and its parsing would finish when it saw the next `+` binary operation.  For initial entry,
583
    /// the `power_min` should be zero.
584
    fn eval_expression(&mut self, power_min: u8, cause: &Token) -> PyResult<Expr> {
2,524✔
585
        let token = self.next_token()?.ok_or_else(|| {
2,524✔
586
            QASM2ParseError::new_err(message_bad_eof(
2✔
587
                Some(&Position::new(
2✔
588
                    self.current_filename(),
2✔
589
                    cause.line,
2✔
590
                    cause.col,
2✔
591
                )),
2✔
592
                if power_min == 0 {
2✔
593
                    "an expression"
2✔
594
                } else {
595
                    "a missing operand"
×
596
                },
597
            ))
598
        })?;
2,522✔
599
        let atom = self.try_atom_from_token(&token)?.ok_or_else(|| {
2,520✔
600
            QASM2ParseError::new_err(message_incorrect_requirement(
10✔
601
                if power_min == 0 {
10✔
UNCOV
602
                    "an expression"
×
603
                } else {
604
                    "a missing operand"
10✔
605
                },
606
                &token,
10✔
607
                self.current_filename(),
10✔
608
            ))
10✔
609
        })?;
2,514✔
610
        // First evaluate the "left-hand side" of a (potential) sequence of binary infix operators.
611
        // This might be a simple value, a unary operator acting on a value, or a bracketed
612
        // expression (either the operand of a function, or just plain parentheses).  This can also
613
        // invoke a recursive call; the parenthesis components feel naturally recursive, and the
614
        // unary operator component introduces a new precedence level that requires a recursive
615
        // call to evaluate.
616
        let mut lhs = match atom {
2,504✔
617
            Atom::LParen => {
618
                let out = self.eval_expression(0, cause)?;
36✔
619
                self.expect(TokenType::RParen, "a closing parenthesis", &token)?;
26✔
620
                Ok(out)
24✔
621
            }
622
            Atom::RParen => {
623
                if power_min == 0 {
12✔
624
                    Err(QASM2ParseError::new_err(message_generic(
2✔
625
                        Some(&Position::new(
2✔
626
                            self.current_filename(),
2✔
627
                            token.line,
2✔
628
                            token.col,
2✔
629
                        )),
2✔
630
                        "did not find an expected expression",
2✔
631
                    )))
2✔
632
                } else {
633
                    Err(QASM2ParseError::new_err(message_generic(
10✔
634
                        Some(&Position::new(
10✔
635
                            self.current_filename(),
10✔
636
                            token.line,
10✔
637
                            token.col,
10✔
638
                        )),
10✔
639
                        "the parenthesis closed, but there was a missing operand",
10✔
640
                    )))
10✔
641
                }
642
            }
643
            Atom::Function(func) => {
112✔
644
                let lparen_token =
112✔
645
                    self.expect(TokenType::LParen, "an opening parenthesis", &token)?;
112✔
646
                let argument = self.eval_expression(0, &token)?;
112✔
647
                let comma = self.accept(TokenType::Comma)?;
110✔
648
                self.check_trailing_comma(comma.as_ref())?;
110✔
649
                self.expect(TokenType::RParen, "a closing parenthesis", &lparen_token)?;
108✔
650
                Ok(self.apply_function(func, argument, &token)?)
108✔
651
            }
652
            Atom::CustomFunction(callable, num_params) => {
56✔
653
                let lparen_token =
56✔
654
                    self.expect(TokenType::LParen, "an opening parenthesis", &token)?;
56✔
655
                let mut arguments = Vec::<Expr>::with_capacity(num_params);
56✔
656
                let mut comma = None;
56✔
657
                loop {
658
                    // There are breaks at the start and end of this loop, because we might be
659
                    // breaking because there are _no_ parameters, because there's a trailing
660
                    // comma before the closing parenthesis, or because we didn't see a comma after
661
                    // an expression so we _need_ a closing parenthesis.
662
                    if let Some((Atom::RParen, _)) = self.peek_atom()? {
74✔
663
                        break;
22✔
664
                    }
52✔
665
                    arguments.push(self.eval_expression(0, &token)?);
52✔
666
                    comma = self.accept(TokenType::Comma)?;
52✔
667
                    if comma.is_none() {
52✔
668
                        break;
34✔
669
                    }
18✔
670
                }
671
                self.check_trailing_comma(comma.as_ref())?;
56✔
672
                self.expect(TokenType::RParen, "a closing parenthesis", &lparen_token)?;
56✔
673
                if arguments.len() == num_params {
56✔
674
                    Ok(self.apply_custom_function(callable, arguments, &token)?)
44✔
675
                } else {
676
                    Err(QASM2ParseError::new_err(message_generic(
12✔
677
                        Some(&Position::new(
12✔
678
                            self.current_filename(),
12✔
679
                            token.line,
12✔
680
                            token.col,
12✔
681
                        )),
12✔
682
                        &format!(
12✔
683
                            "custom function argument-count mismatch: expected {}, saw {}",
12✔
684
                            num_params,
12✔
685
                            arguments.len(),
12✔
686
                        ),
12✔
687
                    )))
12✔
688
                }
689
            }
690
            Atom::Op(op) => match prefix_power(op) {
200✔
691
                Some(power) => {
194✔
692
                    let expr = self.eval_expression(power, &token)?;
194✔
693
                    Ok(self.apply_prefix(op, expr)?)
194✔
694
                }
695
                None => Err(QASM2ParseError::new_err(message_generic(
6✔
696
                    Some(&Position::new(
6✔
697
                        self.current_filename(),
6✔
698
                        token.line,
6✔
699
                        token.col,
6✔
700
                    )),
6✔
701
                    &format!("'{}' is not a valid unary operator", op.text()),
6✔
702
                ))),
6✔
703
            },
704
            Atom::Const(val) => Ok(Expr::Constant(val)),
1,910✔
705
            Atom::Parameter(val) => Ok(Expr::Parameter(val)),
178✔
706
        }?;
30✔
707
        // Now loop over a series of infix operators.  We can continue as long as we're just
708
        // looking at operators that bind more tightly than the `power_min` passed to this
709
        // function.  Once they're the same power or less, we have to return, because the calling
710
        // evaluator needs to bind its operator before we move on to the next infix operator.
711
        while let Some((Atom::Op(op), peeked_token)) = self.peek_atom()? {
2,810✔
712
            let (power_l, power_r) = binary_power(op);
488✔
713
            if power_l < power_min {
488✔
714
                break;
66✔
715
            }
422✔
716
            self.next_token()?; // Skip peeked operator.
422✔
717
            let rhs = self.eval_expression(power_r, &peeked_token)?;
422✔
718
            lhs = self.apply_infix(op, lhs, rhs, &peeked_token)?;
388✔
719
        }
720
        Ok(lhs)
2,388✔
721
    }
2,524✔
722

723
    /// Parse a single expression completely. This is the only public entry point to the
724
    /// operator-precedence parser.
725
    ///
726
    /// .. note::
727
    ///
728
    ///     This evaluates in a floating-point context, including evaluating integer tokens, since
729
    ///     the only places that expressions are valid in OpenQASM 2 is during gate applications.
730
    pub fn parse_expression(&mut self, cause: &Token) -> PyResult<Expr> {
1,708✔
731
        self.eval_expression(0, cause)
1,708✔
732
    }
1,708✔
733
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc