From 6c2aad15838dedd129a8c1e88efa9e300d1a56fa Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 4 Apr 2024 16:56:16 +0100 Subject: [PATCH 1/2] Handle huge integers in OpenQASM 2 expression evaluator This modifies the expression evaluator to directly parse the backing string data of an integer token in a floating-point context, which lets us handle numbers that would otherwise overflow a `usize`. It's possible for this to be totally valid, if, for example, the integer is a multiple of some very large power of two that doesn't overflow a double-precision float. We already needed to immediately cast the integer to a float, so this is just a more accurate way of doing the evaluation, and doesn't affect when we use integers in other contexts. --- crates/qasm2/src/expr.rs | 13 ++++++++++++- crates/qasm2/src/lex.rs | 2 +- .../notes/qasm2-bigint-8eff42acb67903e6.yaml | 9 +++++++++ test/python/qasm2/test_expression.py | 12 ++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index f7faad0c6298..62dca368505d 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -502,7 +502,13 @@ impl<'a> ExprParser<'a> { | TokenType::Sqrt | TokenType::Tan => Ok(Some(Atom::Function(token.ttype.into()))), TokenType::Real => Ok(Some(Atom::Const(token.real(self.context)))), - TokenType::Integer => Ok(Some(Atom::Const(token.int(self.context) as f64))), + // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0 + // integers can be interpreted as floats, and doing that allows us to gracefully handle + // cases where a huge float would overflow a `usize`. Never mind that in such a case, + // there's almost certainly precision loss from the floating-point representating + // having insufficient mantissa digits to faithfully represent the angle mod 2pi; + // that's not our fault in the parser. + TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))), TokenType::Id => { let id = token.text(self.context); @@ -698,6 +704,11 @@ impl<'a> ExprParser<'a> { /// Parse a single expression completely. This is the only public entry point to the /// operator-precedence parser. + /// + /// .. note:: + /// + /// This evaluates in a floating-point context, including evaluating integer tokens, since + /// the only places that expressions are valid in OpenQASM 2 is during gate applications. pub fn parse_expression(&mut self, cause: &Token) -> PyResult { self.eval_expression(0, cause) } diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index 024681b877ff..c01bd1e37263 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -264,7 +264,7 @@ impl Token { /// If the token is a real number, this method can be called to evaluate its value. Panics if /// the token is not a real number. pub fn real(&self, context: &TokenContext) -> f64 { - if self.ttype != TokenType::Real { + if !(self.ttype == TokenType::Real || self.ttype == TokenType::Integer) { panic!() } context.text[self.index].parse().unwrap() diff --git a/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml new file mode 100644 index 000000000000..2fb1b4dcc5a1 --- /dev/null +++ b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The OpenQASM 2.0 parser (:func:`.qasm2.load` and :func:`.qasm2.loads`) can now evaluate + gate-angle expressions including integer operands that would overflow the system-size integer. + These will be evaluated in a double-precision floating-point context, just like the rest of the + expression always has been. Beware: an arbitrarily large integer will not necessarily be + exactly representable in double-precision floating-point, so there is a chance that however the + circuit was generated, it had already lost all numerical precision modulo :math:`2\pi`. diff --git a/test/python/qasm2/test_expression.py b/test/python/qasm2/test_expression.py index 98aead7f3b43..2ef35abde9ec 100644 --- a/test/python/qasm2/test_expression.py +++ b/test/python/qasm2/test_expression.py @@ -123,6 +123,18 @@ def test_function_symbolic(self, function_str, function_py): actual = [float(x) for x in abstract_op.definition.data[0].operation.params] self.assertEqual(list(actual), expected) + def test_bigint(self): + """Test that an expression can be evaluated even if it contains an integer that will + overflow the integer handling.""" + bigint = 1 << 200 + # Sanity check that the number we're trying for is represented at full precision in floating + # point (which it should be - it's a power of two with fewer than 11 bits of exponent). + self.assertEqual(int(float(bigint)), bigint) + program = f"qreg q[1]; U({bigint}, -{bigint}, {bigint} * 2.0) q[0];" + parsed = qiskit.qasm2.loads(program) + parameters = list(parsed.data[0].operation.params) + self.assertEqual([bigint, -bigint, 2 * bigint], parameters) + class TestPrecedenceAssociativity(QiskitTestCase): def test_precedence(self): From 9f4fbf1c678bf728321914678e7bf6bc84d7db09 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 11 Apr 2024 13:42:08 +0100 Subject: [PATCH 2/2] Clarify int/float split --- crates/qasm2/src/expr.rs | 3 +-- crates/qasm2/src/lex.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index 62dca368505d..d8a08080a95c 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -501,14 +501,13 @@ impl<'a> ExprParser<'a> { | TokenType::Sin | TokenType::Sqrt | TokenType::Tan => Ok(Some(Atom::Function(token.ttype.into()))), - TokenType::Real => Ok(Some(Atom::Const(token.real(self.context)))), // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0 // integers can be interpreted as floats, and doing that allows us to gracefully handle // cases where a huge float would overflow a `usize`. Never mind that in such a case, // there's almost certainly precision loss from the floating-point representating // having insufficient mantissa digits to faithfully represent the angle mod 2pi; // that's not our fault in the parser. - TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), + TokenType::Real | TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))), TokenType::Id => { let id = token.text(self.context); diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index c01bd1e37263..f9f674cbc93e 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -262,7 +262,7 @@ impl Token { } /// If the token is a real number, this method can be called to evaluate its value. Panics if - /// the token is not a real number. + /// the token is not a float or an integer. pub fn real(&self, context: &TokenContext) -> f64 { if !(self.ttype == TokenType::Real || self.ttype == TokenType::Integer) { panic!()