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

Qiskit / qiskit / 24124968614

08 Apr 2026 08:09AM UTC coverage: 87.523% (-0.003%) from 87.526%
24124968614

Pull #15968

github

web-flow
Merge f6633e3cc into 8064d27e1
Pull Request #15968: fix: typos in comments

104431 of 119319 relevant lines covered (87.52%)

1022995.64 hits per line

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

93.82
/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 https://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
    Position, QASM2ParseError, message_bad_eof, message_generic, message_incorrect_requirement,
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 {
720✔
96
        match value {
720✔
97
            TokenType::Plus => Op::Plus,
136✔
98
            TokenType::Minus => Op::Minus,
246✔
99
            TokenType::Asterisk => Op::Multiply,
82✔
100
            TokenType::Slash => Op::Divide,
226✔
101
            TokenType::Caret => Op::Power,
30✔
102
            _ => panic!(),
×
103
        }
104
    }
720✔
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> {
450✔
155
        Ok(match self {
450✔
156
            Expr::Constant(value) => bytecode::ExprConstant { value }
166✔
157
                .into_pyobject(py)?
166✔
158
                .into_any(),
166✔
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
    }
450✔
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> {
210✔
226
    match op {
210✔
227
        Op::Plus | Op::Minus => Some(5),
204✔
228
        _ => None,
6✔
229
    }
230
}
210✔
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) {
510✔
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 {
510✔
246
        Op::Plus | Op::Minus => (1, 2),
178✔
247
        Op::Multiply | Op::Divide => (3, 4),
304✔
248
        Op::Power => (8, 7),
28✔
249
    }
250
}
510✔
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,406✔
269
        let mut pointer = self.tokens.len() - 1;
3,406✔
270
        while pointer > 1 {
3,406✔
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,406✔
279
    }
3,406✔
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,126✔
284
        let mut pointer = self.tokens.len() - 1;
3,126✔
285
        while pointer > 1 && self.tokens[pointer].peek(self.context)?.is_none() {
3,126✔
286
            pointer -= 1;
×
287
        }
×
288
        self.tokens[pointer].peek(self.context)
3,126✔
289
    }
3,126✔
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> {
204✔
337
        match prefix {
204✔
338
            Op::Plus => Ok(expr),
18✔
339
            Op::Minus => match expr {
186✔
340
                Expr::Constant(val) => Ok(Expr::Constant(-val)),
178✔
341
                _ => Ok(Expr::Negate(Box::new(expr))),
8✔
342
            },
343
            _ => panic!(),
×
344
        }
345
    }
204✔
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> {
402✔
351
        if let (Expr::Constant(val), Op::Divide) = (&rhs, infix) {
402✔
352
            if *val == 0.0 {
200✔
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
            }
192✔
362
        };
202✔
363
        if let (Expr::Constant(val_l), Expr::Constant(val_r)) = (&lhs, &rhs) {
394✔
364
            // Eagerly constant-fold if possible.
365
            match infix {
332✔
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)),
182✔
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
    }
402✔
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) => match retval.extract::<f64>(py) {
30✔
456
                        Ok(fval) => Ok(Expr::Constant(fval)),
28✔
457
                        Err(inner) => {
2✔
458
                            let error = QASM2ParseError::new_err(message_generic(
2✔
459
                                Some(&Position::new(
2✔
460
                                    self.current_filename(),
2✔
461
                                    token.line,
2✔
462
                                    token.col,
2✔
463
                                )),
2✔
464
                                "user-defined function returned non-float during constant folding",
2✔
465
                            ));
466
                            error.set_cause(py, Some(inner));
2✔
467
                            Err(error)
2✔
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
                        ));
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,550✔
509
        match token.ttype {
5,550✔
510
            TokenType::LParen => Ok(Some(Atom::LParen)),
36✔
511
            TokenType::RParen => Ok(Some(Atom::RParen)),
1,320✔
512
            TokenType::Minus
513
            | TokenType::Plus
514
            | TokenType::Asterisk
515
            | TokenType::Slash
516
            | TokenType::Caret => Ok(Some(Atom::Op(token.ttype.into()))),
720✔
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,728✔
530
            TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))),
272✔
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(
2✔
538
                                self.current_filename(),
2✔
539
                                token.line,
2✔
540
                                token.col,
2✔
541
                            )),
2✔
542
                            &format!("'{id}' is a gate qubit, not a parameter"),
2✔
543
                        )))
2✔
544
                    }
545
                    None => match self.global_symbols.get(id) {
68✔
546
                        Some(GlobalSymbol::Classical {
547
                            callable,
64✔
548
                            num_params,
64✔
549
                        }) => Ok(Some(Atom::CustomFunction(callable.clone(), *num_params))),
64✔
550
                        _ => Err(QASM2ParseError::new_err(message_generic(
4✔
551
                            Some(&Position::new(
4✔
552
                                self.current_filename(),
4✔
553
                                token.line,
4✔
554
                                token.col,
4✔
555
                            )),
4✔
556
                            &format!(
4✔
557
                                "'{id}' is not a parameter or custom instruction defined in this scope",
4✔
558
                            ),
4✔
559
                        ))),
4✔
560
                    },
561
                }
562
            }
563
            _ => Ok(None),
1,100✔
564
        }
565
    }
5,550✔
566

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

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

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