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

Qiskit / qiskit / 10946369059

19 Sep 2024 05:59PM CUT coverage: 88.783% (-0.007%) from 88.79%
10946369059

Pull #13184

github

web-flow
Merge 97d519050 into adf55e191
Pull Request #13184: Don't run ElidePermutation if routing is disabled

2 of 2 new or added lines in 1 file covered. (100.0%)

11 existing lines in 3 files now uncovered.

73484 of 82768 relevant lines covered (88.78%)

359384.67 hits per line

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

94.02
/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 {
672✔
96
        match value {
672✔
97
            TokenType::Plus => Op::Plus,
136✔
98
            TokenType::Minus => Op::Minus,
232✔
99
            TokenType::Asterisk => Op::Multiply,
82✔
100
            TokenType::Slash => Op::Divide,
192✔
101
            TokenType::Caret => Op::Power,
30✔
102
            _ => panic!(),
×
103
        }
104
    }
672✔
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 IntoPy<PyObject> for Expr {
150
    fn into_py(self, py: Python<'_>) -> PyObject {
394✔
151
        match self {
394✔
152
            Expr::Constant(value) => bytecode::ExprConstant { value }.into_py(py),
110✔
153
            Expr::Parameter(index) => bytecode::ExprArgument { index }.into_py(py),
162✔
154
            Expr::Negate(expr) => bytecode::ExprUnary {
8✔
155
                opcode: bytecode::UnaryOpCode::Negate,
8✔
156
                argument: expr.into_py(py),
8✔
157
            }
8✔
158
            .into_py(py),
8✔
159
            Expr::Add(left, right) => bytecode::ExprBinary {
18✔
160
                opcode: bytecode::BinaryOpCode::Add,
18✔
161
                left: left.into_py(py),
18✔
162
                right: right.into_py(py),
18✔
163
            }
18✔
164
            .into_py(py),
18✔
165
            Expr::Subtract(left, right) => bytecode::ExprBinary {
8✔
166
                opcode: bytecode::BinaryOpCode::Subtract,
8✔
167
                left: left.into_py(py),
8✔
168
                right: right.into_py(py),
8✔
169
            }
8✔
170
            .into_py(py),
8✔
171
            Expr::Multiply(left, right) => bytecode::ExprBinary {
14✔
172
                opcode: bytecode::BinaryOpCode::Multiply,
14✔
173
                left: left.into_py(py),
14✔
174
                right: right.into_py(py),
14✔
175
            }
14✔
176
            .into_py(py),
14✔
177
            Expr::Divide(left, right) => bytecode::ExprBinary {
16✔
178
                opcode: bytecode::BinaryOpCode::Divide,
16✔
179
                left: left.into_py(py),
16✔
180
                right: right.into_py(py),
16✔
181
            }
16✔
182
            .into_py(py),
16✔
183
            Expr::Power(left, right) => bytecode::ExprBinary {
6✔
184
                opcode: bytecode::BinaryOpCode::Power,
6✔
185
                left: left.into_py(py),
6✔
186
                right: right.into_py(py),
6✔
187
            }
6✔
188
            .into_py(py),
6✔
189
            Expr::Function(func, expr) => bytecode::ExprUnary {
40✔
190
                opcode: func.into(),
40✔
191
                argument: expr.into_py(py),
40✔
192
            }
40✔
193
            .into_py(py),
40✔
194
            Expr::CustomFunction(func, exprs) => bytecode::ExprCustom {
12✔
195
                callable: func,
12✔
196
                arguments: exprs.into_iter().map(|expr| expr.into_py(py)).collect(),
18✔
197
            }
12✔
198
            .into_py(py),
12✔
199
        }
200
    }
394✔
201
}
202

203
/// Calculate the binding power of an [Op] when used in a prefix position.  Returns [None] if the
204
/// operation cannot be used in the prefix position.  The binding power is on the same scale as
205
/// those returned by [binary_power].
206
fn prefix_power(op: Op) -> Option<u8> {
196✔
207
    match op {
196✔
208
        Op::Plus | Op::Minus => Some(5),
190✔
209
        _ => None,
6✔
210
    }
211
}
196✔
212

213
/// Calculate the binding power of an [Op] when used in an infix position.  The differences between
214
/// left- and right-binding powers represent the associativity of the operation.
215
fn binary_power(op: Op) -> (u8, u8) {
476✔
216
    // For new binding powers, use the odd number as the "base" and the even number one larger than
476✔
217
    // it to represent the associativity.  Left-associative operators bind more strongly to the
476✔
218
    // operand on their right (i.e. in `a + b + c`, the first `+` binds to the `b` more tightly
476✔
219
    // than the second, so we get the left-associative form), and right-associative operators bind
476✔
220
    // more strongly to the operand of their left.  The separation of using the odd--even pair is
476✔
221
    // so there's no clash between different operator levels, even accounting for the associativity
476✔
222
    // distinction.
476✔
223
    //
476✔
224
    // All powers should be greater than zero; we need zero free to be the base case in the
476✔
225
    // entry-point to the precedence parser.
476✔
226
    match op {
476✔
227
        Op::Plus | Op::Minus => (1, 2),
178✔
228
        Op::Multiply | Op::Divide => (3, 4),
270✔
229
        Op::Power => (8, 7),
28✔
230
    }
231
}
476✔
232

233
/// A subparser used to do the operator-precedence part of the parsing for individual parameter
234
/// expressions.  The main parser creates a new instance of this struct for each expression it
235
/// expects, and the instance lives only as long as is required to parse that expression, because
236
/// it takes temporary responsibility for the [TokenStream] that backs the main parser.
237
pub struct ExprParser<'a> {
238
    pub tokens: &'a mut Vec<TokenStream>,
239
    pub context: &'a mut TokenContext,
240
    pub gate_symbols: &'a HashMap<String, GateSymbol>,
241
    pub global_symbols: &'a HashMap<String, GlobalSymbol>,
242
    pub strict: bool,
243
}
244

245
impl<'a> ExprParser<'a> {
246
    /// Get the next token available in the stack of token streams, popping and removing any
247
    /// complete streams, except the base case.  Will only return `None` once all streams are
248
    /// exhausted.
249
    fn next_token(&mut self) -> PyResult<Option<Token>> {
3,276✔
250
        let mut pointer = self.tokens.len() - 1;
3,276✔
251
        while pointer > 1 {
3,276✔
252
            let out = self.tokens[pointer].next(self.context)?;
×
253
            if out.is_some() {
×
254
                return Ok(out);
×
255
            }
×
256
            self.tokens.pop();
×
257
            pointer -= 1;
×
258
        }
259
        self.tokens[0].next(self.context)
3,276✔
260
    }
3,276✔
261

262
    /// Peek the next token in the stack of token streams.  This does not remove any complete
263
    /// streams yet.  Will only return `None` once all streams are exhausted.
264
    fn peek_token(&mut self) -> PyResult<Option<&Token>> {
2,996✔
265
        let mut pointer = self.tokens.len() - 1;
2,996✔
266
        while pointer > 1 && self.tokens[pointer].peek(self.context)?.is_none() {
2,996✔
267
            pointer -= 1;
×
268
        }
×
269
        self.tokens[pointer].peek(self.context)
2,996✔
270
    }
2,996✔
271

272
    /// Get the filename associated with the currently active token stream.
273
    fn current_filename(&self) -> &std::ffi::OsStr {
88✔
274
        &self.tokens[self.tokens.len() - 1].filename
88✔
275
    }
88✔
276

277
    /// Expect a token of the correct [TokenType].  This is a direct analogue of
278
    /// [parse::State::expect].  The error variant of the result contains a suitable error message
279
    /// if the expectation is violated.
280
    fn expect(&mut self, expected: TokenType, required: &str, cause: &Token) -> PyResult<Token> {
358✔
281
        let token = match self.next_token()? {
358✔
282
            None => {
283
                return Err(QASM2ParseError::new_err(message_bad_eof(
×
284
                    Some(&Position::new(
×
285
                        self.current_filename(),
×
286
                        cause.line,
×
287
                        cause.col,
×
288
                    )),
×
289
                    required,
×
290
                )))
×
291
            }
292
            Some(token) => token,
358✔
293
        };
358✔
294
        if token.ttype == expected {
358✔
295
            Ok(token)
356✔
296
        } else {
297
            Err(QASM2ParseError::new_err(message_incorrect_requirement(
2✔
298
                required,
2✔
299
                &token,
2✔
300
                self.current_filename(),
2✔
301
            )))
2✔
302
        }
303
    }
358✔
304

305
    /// Peek the next token from the stream, and consume and return it only if it has the correct
306
    /// type.
307
    fn accept(&mut self, acceptable: TokenType) -> PyResult<Option<Token>> {
162✔
308
        match self.peek_token()? {
162✔
309
            Some(Token { ttype, .. }) if *ttype == acceptable => self.next_token(),
162✔
310
            _ => Ok(None),
140✔
311
        }
312
    }
162✔
313

314
    /// Apply a prefix [Op] to the current [expression][Expr].  If the current expression is a
315
    /// constant floating-point value the application will be eagerly constant-folded, otherwise
316
    /// the resulting [Expr] will have a tree structure.
317
    fn apply_prefix(&mut self, prefix: Op, expr: Expr) -> PyResult<Expr> {
190✔
318
        match prefix {
190✔
319
            Op::Plus => Ok(expr),
18✔
320
            Op::Minus => match expr {
172✔
321
                Expr::Constant(val) => Ok(Expr::Constant(-val)),
164✔
322
                _ => Ok(Expr::Negate(Box::new(expr))),
8✔
323
            },
324
            _ => panic!(),
×
325
        }
326
    }
190✔
327

328
    /// Apply a binary infix [Op] to the current [expression][Expr].  If both operands have
329
    /// constant floating-point values the application will be eagerly constant-folded, otherwise
330
    /// the resulting [Expr] will have a tree structure.
331
    fn apply_infix(&mut self, infix: Op, lhs: Expr, rhs: Expr, op_token: &Token) -> PyResult<Expr> {
380✔
332
        if let (Expr::Constant(val), Op::Divide) = (&rhs, infix) {
380✔
333
            if *val == 0.0 {
178✔
334
                return Err(QASM2ParseError::new_err(message_generic(
8✔
335
                    Some(&Position::new(
8✔
336
                        self.current_filename(),
8✔
337
                        op_token.line,
8✔
338
                        op_token.col,
8✔
339
                    )),
8✔
340
                    "cannot divide by zero",
8✔
341
                )));
8✔
342
            }
170✔
343
        };
202✔
344
        if let (Expr::Constant(val_l), Expr::Constant(val_r)) = (&lhs, &rhs) {
372✔
345
            // Eagerly constant-fold if possible.
346
            match infix {
310✔
347
                Op::Plus => Ok(Expr::Constant(val_l + val_r)),
48✔
348
                Op::Minus => Ok(Expr::Constant(val_l - val_r)),
34✔
349
                Op::Multiply => Ok(Expr::Constant(val_l * val_r)),
50✔
350
                Op::Divide => Ok(Expr::Constant(val_l / val_r)),
160✔
351
                Op::Power => Ok(Expr::Constant(val_l.powf(*val_r))),
18✔
352
            }
353
        } else {
354
            // If not, we have to build a tree.
355
            let id_l = Box::new(lhs);
62✔
356
            let id_r = Box::new(rhs);
62✔
357
            match infix {
62✔
358
                Op::Plus => Ok(Expr::Add(id_l, id_r)),
18✔
359
                Op::Minus => Ok(Expr::Subtract(id_l, id_r)),
8✔
360
                Op::Multiply => Ok(Expr::Multiply(id_l, id_r)),
14✔
361
                Op::Divide => Ok(Expr::Divide(id_l, id_r)),
16✔
362
                Op::Power => Ok(Expr::Power(id_l, id_r)),
6✔
363
            }
364
        }
365
    }
380✔
366

367
    /// Apply a "scientific calculator" built-in function to an [expression][Expr].  If the operand
368
    /// is a constant, the function will be constant-folded to produce a new constant expression,
369
    /// otherwise a tree-form [Expr] is returned.
370
    fn apply_function(&mut self, func: Function, expr: Expr, token: &Token) -> PyResult<Expr> {
108✔
371
        match expr {
108✔
372
            Expr::Constant(val) => match func {
68✔
373
                Function::Cos => Ok(Expr::Constant(val.cos())),
8✔
374
                Function::Exp => Ok(Expr::Constant(val.exp())),
6✔
375
                Function::Ln => {
376
                    if val > 0.0 {
24✔
377
                        Ok(Expr::Constant(val.ln()))
8✔
378
                    } else {
379
                        Err(QASM2ParseError::new_err(message_generic(
16✔
380
                            Some(&Position::new(
16✔
381
                                self.current_filename(),
16✔
382
                                token.line,
16✔
383
                                token.col,
16✔
384
                            )),
16✔
385
                            &format!(
16✔
386
                                "failure in constant folding: cannot take ln of non-positive {}",
16✔
387
                                val
16✔
388
                            ),
16✔
389
                        )))
16✔
390
                    }
391
                }
392
                Function::Sin => Ok(Expr::Constant(val.sin())),
10✔
393
                Function::Sqrt => {
394
                    if val >= 0.0 {
14✔
395
                        Ok(Expr::Constant(val.sqrt()))
6✔
396
                    } else {
397
                        Err(QASM2ParseError::new_err(message_generic(
8✔
398
                            Some(&Position::new(
8✔
399
                                self.current_filename(),
8✔
400
                                token.line,
8✔
401
                                token.col,
8✔
402
                            )),
8✔
403
                            &format!(
8✔
404
                                "failure in constant folding: cannot take sqrt of negative {}",
8✔
405
                                val
8✔
406
                            ),
8✔
407
                        )))
8✔
408
                    }
409
                }
410
                Function::Tan => Ok(Expr::Constant(val.tan())),
6✔
411
            },
412
            _ => Ok(Expr::Function(func, Box::new(expr))),
40✔
413
        }
414
    }
108✔
415

416
    fn apply_custom_function(
44✔
417
        &mut self,
44✔
418
        callable: PyObject,
44✔
419
        exprs: Vec<Expr>,
44✔
420
        token: &Token,
44✔
421
    ) -> PyResult<Expr> {
44✔
422
        if exprs.iter().all(|x| matches!(x, Expr::Constant(_))) {
44✔
423
            // We can still do constant folding with custom user classical functions, we're just
424
            // going to have to acquire the GIL and call the Python object the user gave us right
425
            // now.  We need to explicitly handle any exceptions that might occur from that.
426
            Python::with_gil(|py| {
32✔
427
                let args = PyTuple::new_bound(
32✔
428
                    py,
32✔
429
                    exprs.iter().map(|x| {
32✔
430
                        if let Expr::Constant(val) = x {
22✔
431
                            *val
22✔
432
                        } else {
433
                            unreachable!()
×
434
                        }
435
                    }),
32✔
436
                );
32✔
437
                match callable.call1(py, args) {
32✔
438
                    Ok(retval) => {
30✔
439
                        match retval.extract::<f64>(py) {
30✔
440
                            Ok(fval) => Ok(Expr::Constant(fval)),
28✔
441
                            Err(inner) => {
2✔
442
                                let error = QASM2ParseError::new_err(message_generic(
2✔
443
                                Some(&Position::new(self.current_filename(), token.line, token.col)),
2✔
444
                                "user-defined function returned non-float during constant folding",
2✔
445
                            ));
2✔
446
                                error.set_cause(py, Some(inner));
2✔
447
                                Err(error)
2✔
448
                            }
449
                        }
450
                    }
451
                    Err(inner) => {
2✔
452
                        let error = QASM2ParseError::new_err(message_generic(
2✔
453
                            Some(&Position::new(
2✔
454
                                self.current_filename(),
2✔
455
                                token.line,
2✔
456
                                token.col,
2✔
457
                            )),
2✔
458
                            "caught exception when constant folding with user-defined function",
2✔
459
                        ));
2✔
460
                        error.set_cause(py, Some(inner));
2✔
461
                        Err(error)
2✔
462
                    }
463
                }
464
            })
32✔
465
        } else {
466
            Ok(Expr::CustomFunction(callable, exprs))
12✔
467
        }
468
    }
44✔
469

470
    /// If in `strict` mode, and we have a trailing comma, emit a suitable error message.
471
    fn check_trailing_comma(&self, comma: Option<&Token>) -> PyResult<()> {
166✔
472
        match (self.strict, comma) {
166✔
473
            (true, Some(token)) => Err(QASM2ParseError::new_err(message_generic(
2✔
474
                Some(&Position::new(
2✔
475
                    self.current_filename(),
2✔
476
                    token.line,
2✔
477
                    token.col,
2✔
478
                )),
2✔
479
                "[strict] trailing commas in parameter and qubit lists are forbidden",
2✔
480
            ))),
2✔
481
            _ => Ok(()),
164✔
482
        }
483
    }
166✔
484

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

543
    /// Peek at the next [Atom] (and backing [Token]) if the next token exists and can be converted
544
    /// into a valid [Atom].  If it can't, or if we are at the end of the input, the `None` variant
545
    /// is returned.
546
    fn peek_atom(&mut self) -> PyResult<Option<(Atom, Token)>> {
2,834✔
547
        if let Some(&token) = self.peek_token()? {
2,834✔
548
            if let Ok(Some(atom)) = self.try_atom_from_token(&token) {
2,834✔
549
                Ok(Some((atom, token)))
1,782✔
550
            } else {
551
                Ok(None)
1,052✔
552
            }
553
        } else {
554
            Ok(None)
×
555
        }
556
    }
2,834✔
557

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

704
    /// Parse a single expression completely. This is the only public entry point to the
705
    /// operator-precedence parser.
706
    ///
707
    /// .. note::
708
    ///
709
    ///     This evaluates in a floating-point context, including evaluating integer tokens, since
710
    ///     the only places that expressions are valid in OpenQASM 2 is during gate applications.
711
    pub fn parse_expression(&mut self, cause: &Token) -> PyResult<Expr> {
1,678✔
712
        self.eval_expression(0, cause)
1,678✔
713
    }
1,678✔
714
}
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

© 2025 Coveralls, Inc