diff --git a/CHANGELOG.md b/CHANGELOG.md index 7470c32f..e00bcb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,172 +15,23 @@ Types of changes: ## Unreleased ### Added -- Added support for classical declarations with measurement ([#120](https://github.com/qBraid/pyqasm/pull/120)). Usage example - - -```python -In [1]: from pyqasm import loads, dumps - -In [2]: module = loads( - ...: """OPENQASM 3.0; - ...: qubit q; - ...: bit b = measure q; - ...: """) - -In [3]: module.unroll() - -In [4]: dumps(module).splitlines() -Out[4]: ['OPENQASM 3.0;', 'qubit[1] q;', 'bit[1] b;', 'b[0] = measure q[0];'] -``` - -- Added support for `gphase`, `toffoli`, `not`, `c3sx` and `c4x` gates ([#86](https://github.com/qBraid/pyqasm/pull/86)) -- Added a `remove_includes` method to `QasmModule` to remove include statements from the generated QASM code ([#100](https://github.com/qBraid/pyqasm/pull/100)). Usage example - - -```python -In [1]: from pyqasm import loads - -In [2]: module = loads( - ...: """OPENQASM 3.0; - ...: include "stdgates.inc"; - ...: include "random.qasm"; - ...: - ...: qubit[2] q; - ...: h q; - ...: """) - -In [3]: module.remove_includes() -Out[3]: - -In [4]: from pyqasm import dumps - -In [5]: dumps(module).splitlines() -Out[5]: ['OPENQASM 3.0;', 'qubit[2] q;', 'h q;'] -``` -- Added support for unrolling multi-bit branching with `==`, `>=`, `<=`, `>`, and `<` ([#112](https://github.com/qBraid/pyqasm/pull/112)). Usage example - -```python -In [1]: from pyqasm import loads - -In [2]: module = loads( - ...: """OPENQASM 3.0; - ...: include "stdgates.inc"; - ...: qubit[1] q; - ...: bit[4] c; - ...: if(c == 3){ - ...: h q[0]; - ...: } - ...: """) - -In [3]: module.unroll() - -In [4]: dumps(module) -OPENQASM 3.0; -include "stdgates.inc"; -qubit[1] q; -bit[4] c; -if (c[0] == false) { - if (c[1] == false) { - if (c[2] == true) { - if (c[3] == true) { - h q[0]; - } - } - } -} -``` -- Add formatting check for Unix style line endings i.e. `\n`. For any other line endings, errors are raised. ([#130](https://github.com/qBraid/pyqasm/pull/130)) -- Add `rebase` method to the `QasmModule`. Users now have the ability to rebase the quantum programs to any of the available `pyqasm.elements.BasisSet` ([#123](https://github.com/qBraid/pyqasm/pull/123)). Usage example - - -```python -In [9] : import pyqasm - -In [10]: qasm_input = """ OPENQASM 3.0; - ...: include "stdgates.inc"; - ...: qubit[2] q; - ...: bit[2] c; - ...: - ...: h q; - ...: x q; - ...: cz q[0], q[1]; - ...: - ...: c = measure q; - ...: """ - -In [11]: module = pyqasm.loads(qasm_input) - -In [12]: from pyqasm.elements import BasisSet - -In [13]: module.rebase(target_basis_set=BasisSet.ROTATIONAL_CX) -Out[13]: - -In [14]: print(pyqasm.dumps(module)) -OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -bit[2] c; -ry(1.5707963267948966) q[0]; -rx(3.141592653589793) q[0]; -ry(1.5707963267948966) q[1]; -rx(3.141592653589793) q[1]; -rx(3.141592653589793) q[0]; -rx(3.141592653589793) q[1]; -ry(1.5707963267948966) q[1]; -rx(3.141592653589793) q[1]; -cx q[0], q[1]; -ry(1.5707963267948966) q[1]; -rx(3.141592653589793) q[1]; -c[0] = measure q[0]; -c[1] = measure q[1]; -``` - -Current support for `BasisSet.CLIFFORD_T` decompositions is limited to non-parameterized gates only. -- Added `.gitattributes` file to specify unix-style line endings(`\n`) for all files ([#123](https://github.com/qBraid/pyqasm/pull/123)) -- Added support for `ctrl` modifiers. QASM3 programs with `ctrl @` modifiers can now be loaded as `QasmModule` objects ([#121](https://github.com/qBraid/pyqasm/pull/121)). Usage example - - -```python -In [18]: import pyqasm - -In [19]: qasm3_string = """ - ...: OPENQASM 3.0; - ...: include "stdgates.inc"; - ...: qubit[3] q; - ...: gate custom a, b, c { - ...: ctrl @ x a, b; - ...: ctrl(2) @ x a, b, c; - ...: } - ...: custom q[0], q[1], q[2]; - ...: """ - -In [20]: module = pyqasm.loads(qasm3_string) - -In [21]: module.unroll() - -In [22]: print(pyqasm.dumps(module)) -OPENQASM 3.0; -include "stdgates.inc"; -qubit[3] q; -cx q[0], q[1]; -ccx q[0], q[1], q[2]; -``` ### Improved / Modified - - Refactored the initialization of `QasmModule` to remove default include statements. Only user supplied include statements are now added to the generated QASM code ([#86](https://github.com/qBraid/pyqasm/pull/86)) -- Update the `pre-release.yml` workflow to multi-platform builds. Added the pre-release version bump to the `pre_build.sh` script. ([#99](https://github.com/qBraid/pyqasm/pull/99)) -- Bumped qBraid-CLI dep in `tox.ini` to fix `qbraid headers` command formatting bug ([#129](https://github.com/qBraid/pyqasm/pull/129)) +- Re-wrote the `QasmAnalyzer.extract_qasm_version` method so that it extracts the program version just by looking at the [first non-comment line](https://github.com/openqasm/openqasm/blob/bb923eb9a84fdffe1ba6fc3c20d0b47a131523d9/source/language/comments.rst#version-string), instead of parsing the entire program ([#140](https://github.com/qBraid/pyqasm/pull/140)). ### Deprecated ### Removed -- Unix-style line endings check in GitHub actions was removed in lieu of the `.gitattributes` file ([#123](https://github.com/qBraid/pyqasm/pull/123)) ### Fixed -- Fixed bugs in implementations of `gpi2` and `prx` gates ([#86](https://github.com/qBraid/pyqasm/pull/86)) ### Dependencies -- Update sphinx-autodoc-typehints requirement from <2.6,>=1.24 to >=1.24,<3.1 ([#119](https://github.com/qBraid/pyqasm/pull/119)) ## Past Release Notes Archive of changelog entries from previous releases: +- [v0.2.0](https://github.com/qBraid/pyqasm/releases/tag/v0.2.0) - [v0.1.0](https://github.com/qBraid/pyqasm/releases/tag/v0.1.0) - [v0.1.0-alpha](https://github.com/qBraid/pyqasm/releases/tag/v0.1.0-alpha) - [v0.0.3](https://github.com/qBraid/pyqasm/releases/tag/v0.0.3) diff --git a/src/pyqasm/analyzer.py b/src/pyqasm/analyzer.py index 9c8b71bf..91d8a12a 100644 --- a/src/pyqasm/analyzer.py +++ b/src/pyqasm/analyzer.py @@ -14,10 +14,10 @@ """ from __future__ import annotations +import re from typing import TYPE_CHECKING, Any, Optional, Union import numpy as np -from openqasm3 import parse from openqasm3.ast import ( DiscreteSet, Expression, @@ -28,7 +28,6 @@ QuantumMeasurementStatement, RangeDefinition, ) -from openqasm3.parser import QASM3ParsingError from pyqasm.exceptions import QasmParsingError, ValidationError, raise_qasm3_error @@ -209,27 +208,29 @@ def get_op_bit_list(operation): ) return bit_list - @staticmethod - def extract_qasm_version(qasm: str) -> int: # type: ignore # pylint: disable=R1710 + @staticmethod # pylint: disable-next=inconsistent-return-statements + def extract_qasm_version(qasm: str) -> float: # type: ignore[return] """ - Parses an OpenQASM program string to determine its major version, either 2 or 3. + Extracts the OpenQASM version from a given OpenQASM string. Args: - qasm (str): The OpenQASM program string. + qasm (str): The OpenQASM program as a string. Returns: - int: The OpenQASM version as an integer. - - Raises: - QasmError: If the string does not represent a valid OpenQASM program. + The semantic version as a float. """ - try: - # TODO: optimize this to just check the start of the program for version - parsed_program = parse(qasm) - assert parsed_program.version is not None - version = int(float(parsed_program.version)) - return version - except (QASM3ParsingError, ValueError, TypeError): - raise_qasm3_error( - "Could not determine the OpenQASM version.", err_type=QasmParsingError - ) + qasm = re.sub(r"//.*", "", qasm) + qasm = re.sub(r"/\*.*?\*/", "", qasm, flags=re.DOTALL) + + lines = qasm.strip().splitlines() + + for line in lines: + line = line.strip() + if line.startswith("OPENQASM"): + match = re.match(r"OPENQASM\s+(\d+)(?:\.(\d+))?;", line) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + return float(f"{major}.{minor}") + + raise_qasm3_error("Could not determine the OpenQASM version.", err_type=QasmParsingError) diff --git a/tests/qasm3/test_version.py b/tests/qasm3/test_version.py index de2cf8cf..98e330c9 100644 --- a/tests/qasm3/test_version.py +++ b/tests/qasm3/test_version.py @@ -19,21 +19,68 @@ from pyqasm.exceptions import QasmParsingError -def test_extract_version(): - """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement.""" +@pytest.mark.parametrize( + "qasm_input, expected_version", + [ + ( + """ + OPENQASM 3.0; + qubit q; + """, + 3.0, + ), + ( + """ + OPENQASM 2; + qubit q; + """, + 2.0, + ), + ( + """ + // Single-line comment + OPENQASM 1.5; // Inline comment + qubit q; + """, + 1.5, + ), + ( + """ + /* + Block comment before the version string + describing the program. + */ + OPENQASM 3.2; + qubit q; + """, + 3.2, + ), + ], +) +def test_extract_qasm_version_valid(qasm_input, expected_version): + """Test valid OpenQASM version extraction with various inputs.""" + assert Qasm3Analyzer.extract_qasm_version(qasm_input) == expected_version - qasm3_program = """ - OPENQASM 3.0; - qubit q; - """ - assert Qasm3Analyzer.extract_qasm_version(qasm3_program) == 3 - -def test_invalid_raises_raises_err(): - """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement.""" - - qasm3_program = """ - random string - """ +@pytest.mark.parametrize( + "qasm_input", + [ + """ + random string + """, + """ + OPENQASM three.point.zero; + qubit q; + """, + """ + OPENQASM 2.0 + qubit q; + """, + "", + " \n \t ", + ], +) +def test_extract_qasm_version_invalid(qasm_input): + """Test invalid OpenQASM inputs that should raise QasmParsingError.""" with pytest.raises(QasmParsingError): - Qasm3Analyzer.extract_qasm_version(qasm3_program) + Qasm3Analyzer.extract_qasm_version(qasm_input)