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

Qiskit / qiskit / 14198856837

01 Apr 2025 02:51PM CUT coverage: 88.046% (-0.005%) from 88.051%
14198856837

Pull #13474

github

web-flow
Merge 66f3aba29 into 040c9c8e8
Pull Request #13474: Migrate CI to Github Actions

72798 of 82682 relevant lines covered (88.05%)

366315.82 hits per line

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

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

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

17
use core::f64;
18

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

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

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

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

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

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

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

94
impl From<TokenType> for Op {
95
    fn from(value: TokenType) -> Self {
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(PyObject, usize),
126
    Op(Op),
127
    Const(f64),
128
    Parameter(ParamId),
129
}
130

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

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

154
    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
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
            }
8✔
166
            .into_pyobject(py)?
8✔
167
            .into_any(),
8✔
168
            Expr::Add(left, right) => bytecode::ExprBinary {
18✔
169
                opcode: bytecode::BinaryOpCode::Add,
18✔
170
                left: left.into_pyobject(py)?.unbind(),
18✔
171
                right: right.into_pyobject(py)?.unbind(),
18✔
172
            }
18✔
173
            .into_pyobject(py)?
18✔
174
            .into_any(),
18✔
175
            Expr::Subtract(left, right) => bytecode::ExprBinary {
8✔
176
                opcode: bytecode::BinaryOpCode::Subtract,
8✔
177
                left: left.into_pyobject(py)?.unbind(),
8✔
178
                right: right.into_pyobject(py)?.unbind(),
8✔
179
            }
8✔
180
            .into_pyobject(py)?
8✔
181
            .into_any(),
8✔
182
            Expr::Multiply(left, right) => bytecode::ExprBinary {
14✔
183
                opcode: bytecode::BinaryOpCode::Multiply,
14✔
184
                left: left.into_pyobject(py)?.unbind(),
14✔
185
                right: right.into_pyobject(py)?.unbind(),
14✔
186
            }
14✔
187
            .into_pyobject(py)?
14✔
188
            .into_any(),
14✔
189
            Expr::Divide(left, right) => bytecode::ExprBinary {
16✔
190
                opcode: bytecode::BinaryOpCode::Divide,
16✔
191
                left: left.into_pyobject(py)?.unbind(),
16✔
192
                right: right.into_pyobject(py)?.unbind(),
16✔
193
            }
16✔
194
            .into_pyobject(py)?
16✔
195
            .into_any(),
16✔
196
            Expr::Power(left, right) => bytecode::ExprBinary {
6✔
197
                opcode: bytecode::BinaryOpCode::Power,
6✔
198
                left: left.into_pyobject(py)?.unbind(),
6✔
199
                right: right.into_pyobject(py)?.unbind(),
6✔
200
            }
6✔
201
            .into_pyobject(py)?
6✔
202
            .into_any(),
6✔
203
            Expr::Function(func, expr) => bytecode::ExprUnary {
40✔
204
                opcode: func.into(),
40✔
205
                argument: expr.into_pyobject(py)?.unbind(),
40✔
206
            }
40✔
207
            .into_pyobject(py)?
40✔
208
            .into_any(),
40✔
209
            Expr::CustomFunction(func, exprs) => bytecode::ExprCustom {
12✔
210
                callable: func,
12✔
211
                arguments: exprs
12✔
212
                    .into_iter()
12✔
213
                    .map(|expr| expr.into_pyobject(py).unwrap().unbind())
18✔
214
                    .collect(),
12✔
215
            }
12✔
216
            .into_pyobject(py)?
12✔
217
            .into_any(),
12✔
218
        })
219
    }
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
498✔
236
    // it to represent the associativity.  Left-associative operators bind more strongly to the
498✔
237
    // operand on their right (i.e. in `a + b + c`, the first `+` binds to the `b` more tightly
498✔
238
    // than the second, so we get the left-associative form), and right-associative operators bind
498✔
239
    // more strongly to the operand of their left.  The separation of using the odd--even pair is
498✔
240
    // so there's no clash between different operator levels, even accounting for the associativity
498✔
241
    // distinction.
498✔
242
    //
498✔
243
    // All powers should be greater than zero; we need zero free to be the base case in the
498✔
244
    // entry-point to the precedence parser.
498✔
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
        };
358✔
313
        if token.ttype == expected {
358✔
314
            Ok(token)
356✔
315
        } else {
316
            Err(QASM2ParseError::new_err(message_incorrect_requirement(
2✔
317
                required,
2✔
318
                &token,
2✔
319
                self.current_filename(),
2✔
320
            )))
2✔
321
        }
322
    }
358✔
323

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

333
    /// Apply a prefix [Op] to the current [expression][Expr].  If the current expression is a
334
    /// constant floating-point value the application will be eagerly constant-folded, otherwise
335
    /// the resulting [Expr] will have a tree structure.
336
    fn apply_prefix(&mut self, prefix: Op, expr: Expr) -> PyResult<Expr> {
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 {}",
16✔
406
                                val
16✔
407
                            ),
16✔
408
                        )))
16✔
409
                    }
410
                }
411
                Function::Sin => Ok(Expr::Constant(val.sin())),
10✔
412
                Function::Sqrt => {
413
                    if val >= 0.0 {
14✔
414
                        Ok(Expr::Constant(val.sqrt()))
6✔
415
                    } else {
416
                        Err(QASM2ParseError::new_err(message_generic(
8✔
417
                            Some(&Position::new(
8✔
418
                                self.current_filename(),
8✔
419
                                token.line,
8✔
420
                                token.col,
8✔
421
                            )),
8✔
422
                            &format!(
8✔
423
                                "failure in constant folding: cannot take sqrt of negative {}",
8✔
424
                                val
8✔
425
                            ),
8✔
426
                        )))
8✔
427
                    }
428
                }
429
                Function::Tan => Ok(Expr::Constant(val.tan())),
6✔
430
            },
431
            _ => Ok(Expr::Function(func, Box::new(expr))),
40✔
432
        }
433
    }
108✔
434

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

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

504
    /// Convert the given general [Token] into the expression-parser-specific [Atom], if possible.
505
    /// Not all [Token]s have a corresponding [Atom]; if this is the case, the return value is
506
    /// `Ok(None)`.  The error variant is returned if the next token is grammatically valid, but
507
    /// not semantically, such as an identifier for a value of an incorrect type.
508
    fn try_atom_from_token(&self, token: &Token) -> PyResult<Option<Atom>> {
5,494✔
509
        match token.ttype {
5,494✔
510
            TokenType::LParen => Ok(Some(Atom::LParen)),
36✔
511
            TokenType::RParen => Ok(Some(Atom::RParen)),
1,300✔
512
            TokenType::Minus
513
            | TokenType::Plus
514
            | TokenType::Asterisk
515
            | TokenType::Slash
516
            | TokenType::Caret => Ok(Some(Atom::Op(token.ttype.into()))),
704✔
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,716✔
530
            TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))),
264✔
531
            TokenType::Id => {
532
                let id = token.text(self.context);
258✔
533
                match self.gate_symbols.get(id) {
258✔
534
                    Some(GateSymbol::Parameter { index }) => Ok(Some(Atom::Parameter(*index))),
188✔
535
                    Some(GateSymbol::Qubit { .. }) => {
536
                        Err(QASM2ParseError::new_err(message_generic(
2✔
537
                            Some(&Position::new(self.current_filename(), token.line, token.col)),
2✔
538
                            &format!("'{}' is a gate qubit, not a parameter", id),
2✔
539
                        )))
2✔
540
                    }
541
                    None => {
542
                        match self.global_symbols.get(id) {
68✔
543
                            Some(GlobalSymbol::Classical { callable, num_params }) => {
64✔
544
                                Ok(Some(Atom::CustomFunction(callable.clone(), *num_params)))
64✔
545
                            }
546
                            _ =>  {
547
                            Err(QASM2ParseError::new_err(message_generic(
4✔
548
                            Some(&Position::new(self.current_filename(), token.line, token.col)),
4✔
549
                            &format!(
4✔
550
                                "'{}' is not a parameter or custom instruction defined in this scope",
4✔
551
                                id,
4✔
552
                            ))))
4✔
553
                            }
554
                    }
555
                }
556
            }
557
            }
558
            _ => Ok(None),
1,100✔
559
        }
560
    }
5,494✔
561

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

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

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

© 2025 Coveralls, Inc