diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cbc99ae..0c86e043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Types of changes: ### Removed ### Fixed +- Fixed a `duration` initialized from arithmetic on a `stretch` operand, such as `duration b = a + s;` or `2 * s`, raising a bare `TypeError: unsupported operand type(s) for +: 'float' and 'NoneType'`. A `stretch` has no value until scheduling, so it now raises a `ValidationError` naming the variable ([#417](https://github.com/qBraid/pyqasm/pull/417), [#389](https://github.com/qBraid/pyqasm/issues/389)). - Decompose `p`, `phaseshift`, and `u1` (`U1`) gates into a single `rz(theta)` rotation instead of `h; rx(theta); h` ([#470](https://github.com/qBraid/pyqasm/pull/470), [#469](https://github.com/qBraid/pyqasm/issues/469)). - Fixed `rebase` failing on `ccx`, `id`, and the `p` gates produced by unrolling `c3x` for the `ROTATIONAL_CX` / `CLIFFORD_T` basis sets ([#468](https://github.com/qBraid/pyqasm/pull/468), [#467](https://github.com/qBraid/pyqasm/issues/467)). - `loads()` syntax errors now name the offending token, line and column, e.g. `Failed to parse OpenQASM string: syntax error near ';' at line 3, column 13`. Before, the reason after the colon was empty ([#460](https://github.com/qBraid/pyqasm/pull/460)). diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index c6319f13..b7a01470 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -471,6 +471,19 @@ def _get_external_function_return_type(expression): return (None, []) statements.extend(rhs_statements) + for operand in (expression.lhs, expression.rhs): + if not isinstance(operand, Identifier): + continue + assert cls.visitor_obj + var = cls.visitor_obj._scope_manager.get_from_visible_scope(operand.name) + # const stretch declarations carry a value, so only reject unresolved ones + if Qasm3Validator.validate_variable_type(var, StretchType) and var.value is None: + raise_qasm3_error( + f"Arithmetic on 'stretch' operand '{operand.name}' is not yet supported", + err_type=ValidationError, + error_node=expression, + span=expression.span, + ) return _check_and_return_value( qasm3_expression_op_map(expression.op.name, lhs_value, rhs_value) ) diff --git a/tests/qasm3/declarations/test_classical.py b/tests/qasm3/declarations/test_classical.py index 37696c42..0c084d51 100644 --- a/tests/qasm3/declarations/test_classical.py +++ b/tests/qasm3/declarations/test_classical.py @@ -518,6 +518,64 @@ def test_stretch_type_error(qasm_code, error_message, error_span, caplog): assert error_span in caplog.text +@pytest.mark.parametrize( + "qasm_code,stretch_var,error_span", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + stretch s; + duration a = 300ns; + duration b = a + s; + """, + "s", + r"Error at line 6, column 25", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + stretch s; + duration b = 2 * s; + """, + "s", + r"Error at line 5, column 25", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + stretch s; + duration a = 300ns; + duration b = a + 2 * s; + """, + "s", + r"Error at line 6, column 29", + ), + ], +) +def test_stretch_arithmetic_not_supported(qasm_code, stretch_var, error_span, caplog): + with pytest.raises(ValidationError) as err: + with caplog.at_level("ERROR"): + loads(qasm_code).validate() + cause = err.value.__cause__ + assert cause is not None + assert f"Arithmetic on 'stretch' operand '{stretch_var}' is not yet supported" in str(cause) + assert error_span in caplog.text + + +def test_const_stretch_arithmetic(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + const stretch s = 100ns; + duration a = 300ns; + duration b = a + 2 * s; + """ + loads(qasm3_string).validate() + + @pytest.mark.parametrize( "qasm_code,error_message,error_span", [