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

Qiskit / qiskit / 17851777282

19 Sep 2025 07:05AM UTC coverage: 88.289% (-0.005%) from 88.294%
17851777282

push

github

web-flow
Use QSD from rust in default unitary synthesis plugin (#15003)

With QSD ported to rust we no longer need to use python to run QSD in
the default unitary synthesis plugin which is written in rust. This
commit updates the unitary synthesis code to directly call qsd from
rust. Doing this also enables the C API transpiler from handling 3+ q
unitaries. The handling around multiqubit unitaries is updated to
indicate they are now supported.

9 of 14 new or added lines in 1 file covered. (64.29%)

11 existing lines in 4 files now uncovered.

92767 of 105072 relevant lines covered (88.29%)

869706.53 hits per line

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

93.63
/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 {
704✔
96
        match value {
704✔
97
            TokenType::Plus => Op::Plus,
136✔
98
            TokenType::Minus => Op::Minus,
242✔
99
            TokenType::Asterisk => Op::Multiply,
82✔
100
            TokenType::Slash => Op::Divide,
214✔
101
            TokenType::Caret => Op::Power,
30✔
102
            _ => panic!(),
×
103
        }
104
    }
704✔
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(Py<PyAny>, 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(Py<PyAny>, 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> {
440✔
155
        Ok(match self {
440✔
156
            Expr::Constant(value) => bytecode::ExprConstant { value }
156✔
157
                .into_pyobject(py)?
156✔
158
                .into_any(),
156✔
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
            }
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
            }
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
            }
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
            }
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
            }
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
            }
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
            }
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
            }
216
            .into_pyobject(py)?
12✔
217
            .into_any(),
12✔
218
        })
219
    }
440✔
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> {
206✔
226
    match op {
206✔
227
        Op::Plus | Op::Minus => Some(5),
200✔
228
        _ => None,
6✔
229
    }
230
}
206✔
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) {
498✔
235
    // For new binding powers, use the odd number as the "base" and the even number one larger than
236
    // it to represent the associativity.  Left-associative operators bind more strongly to the
237
    // operand on their right (i.e. in `a + b + c`, the first `+` binds to the `b` more tightly
238
    // than the second, so we get the left-associative form), and right-associative operators bind
239
    // more strongly to the operand of their left.  The separation of using the odd--even pair is
240
    // so there's no clash between different operator levels, even accounting for the associativity
241
    // distinction.
242
    //
243
    // All powers should be greater than zero; we need zero free to be the base case in the
244
    // entry-point to the precedence parser.
245
    match op {
498✔
246
        Op::Plus | Op::Minus => (1, 2),
178✔
247
        Op::Multiply | Op::Divide => (3, 4),
292✔
248
        Op::Power => (8, 7),
28✔
249
    }
250
}
498✔
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,374✔
269
        let mut pointer = self.tokens.len() - 1;
3,374✔
270
        while pointer > 1 {
3,374✔
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,374✔
279
    }
3,374✔
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,094✔
284
        let mut pointer = self.tokens.len() - 1;
3,094✔
285
        while pointer > 1 && self.tokens[pointer].peek(self.context)?.is_none() {
3,094✔
286
            pointer -= 1;
×
287
        }
×
288
        self.tokens[pointer].peek(self.context)
3,094✔
289
    }
3,094✔
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
        };
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> {
200✔
337
        match prefix {
200✔
338
            Op::Plus => Ok(expr),
18✔
339
            Op::Minus => match expr {
182✔
340
                Expr::Constant(val) => Ok(Expr::Constant(-val)),
174✔
341
                _ => Ok(Expr::Negate(Box::new(expr))),
8✔
342
            },
343
            _ => panic!(),
×
344
        }
345
    }
200✔
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> {
394✔
351
        if let (Expr::Constant(val), Op::Divide) = (&rhs, infix) {
394✔
352
            if *val == 0.0 {
192✔
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
            }
184✔
362
        };
202✔
363
        if let (Expr::Constant(val_l), Expr::Constant(val_r)) = (&lhs, &rhs) {
386✔
364
            // Eagerly constant-fold if possible.
365
            match infix {
324✔
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)),
174✔
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
    }
394✔
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 {val}"
16✔
406
                            ),
16✔
407
                        )))
16✔
408
                    }
409
                }
410
                Function::Sin => Ok(Expr::Constant(val.sin())),
10✔
411
                Function::Sqrt => {
412
                    if val >= 0.0 {
14✔
413
                        Ok(Expr::Constant(val.sqrt()))
6✔
414
                    } else {
415
                        Err(QASM2ParseError::new_err(message_generic(
8✔
416
                            Some(&Position::new(
8✔
417
                                self.current_filename(),
8✔
418
                                token.line,
8✔
419
                                token.col,
8✔
420
                            )),
8✔
421
                            &format!(
8✔
422
                                "failure in constant folding: cannot take sqrt of negative {val}"
8✔
423
                            ),
8✔
424
                        )))
8✔
425
                    }
426
                }
427
                Function::Tan => Ok(Expr::Constant(val.tan())),
6✔
428
            },
429
            _ => Ok(Expr::Function(func, Box::new(expr))),
40✔
430
        }
431
    }
108✔
432

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

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

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

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

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

720
    /// Parse a single expression completely. This is the only public entry point to the
721
    /// operator-precedence parser.
722
    ///
723
    /// .. note::
724
    ///
725
    ///     This evaluates in a floating-point context, including evaluating integer tokens, since
726
    ///     the only places that expressions are valid in OpenQASM 2 is during gate applications.
727
    pub fn parse_expression(&mut self, cause: &Token) -> PyResult<Expr> {
1,738✔
728
        self.eval_expression(0, cause)
1,738✔
729
    }
1,738✔
730
}
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