From 97ae4f65a2fdd686112b187d65119d7acca48744 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 26 Aug 2026 00:09:18 -0700 Subject: [PATCH 1/3] Raise a validation error for stretch arithmetic instead of a TypeError Signed-off-by: Sai Asish Y --- src/pyqasm/expressions.py | 42 ++++++++++++++++++++++ tests/qasm3/declarations/test_classical.py | 36 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index c6319f13..c9ec2a50 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -188,6 +188,39 @@ def _get_var_value(cls, var_name, indices, expression): ) return var_value + @classmethod + def _find_stretch_operand(cls, expression): + """Return the name of a 'stretch' variable referenced in ``expression``, if any. + + A ``stretch`` resolves to its span only at schedule time, so its value stays + ``None`` during evaluation. When such an operand reaches arithmetic it cannot be + folded to a compile-time constant, so this locates the stretch variable in order + to report it. + + Args: + expression: The expression (or sub-expression) to inspect. + + Returns: + str | None: The name of a referenced stretch variable, or ``None``. + """ + var_name = None + if isinstance(expression, Identifier): + var_name = expression.name + elif isinstance(expression, IndexExpression): + var_name, _ = Qasm3Analyzer.analyze_index_expression(expression) + elif isinstance(expression, BinaryExpression): + return cls._find_stretch_operand(expression.lhs) or cls._find_stretch_operand( + expression.rhs + ) + elif isinstance(expression, UnaryExpression): + return cls._find_stretch_operand(expression.expression) + + if var_name is not None: + var = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name) + if var is not None and isinstance(var.base_type, StretchType): + return var_name + return None + @classmethod # pylint: disable-next=too-many-return-statements,too-many-branches,too-many-statements,too-many-locals,too-many-arguments def evaluate_expression( # type: ignore[return] @@ -471,6 +504,15 @@ def _get_external_function_return_type(expression): return (None, []) statements.extend(rhs_statements) + if lhs_value is None or rhs_value is None: + stretch_name = cls._find_stretch_operand(expression) + if stretch_name is not None: + raise_qasm3_error( + f"Arithmetic on 'stretch' operand '{stretch_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..e30b5e73 100644 --- a/tests/qasm3/declarations/test_classical.py +++ b/tests/qasm3/declarations/test_classical.py @@ -518,6 +518,42 @@ 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", + ), + ], +) +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 + + @pytest.mark.parametrize( "qasm_code,error_message,error_span", [ From 955dc3b4e0f7ba6933da64904867eb49de03a664 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sun, 25 Oct 2026 02:25:26 -0700 Subject: [PATCH 2/3] Add changelog entry for stretch arithmetic validation error Signed-off-by: Sai Asish Y --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c14cfada..eb457114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,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)). - 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)). - Fixed the matplotlib printer's controlled-gate lookup for `c3x` and `c4x` ([#458](https://github.com/qBraid/pyqasm/pull/458), [#322](https://github.com/qBraid/pyqasm/issues/322)). From 5581df062bc4efbc873c0db23eaa1b07b6b7111d Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 28 Oct 2026 09:41:08 -0700 Subject: [PATCH 3/3] Use the scope manager to detect stretch operands Signed-off-by: Sai Asish Y --- src/pyqasm/expressions.py | 45 ++++------------------ tests/qasm3/declarations/test_classical.py | 22 +++++++++++ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index c9ec2a50..b7a01470 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -188,39 +188,6 @@ def _get_var_value(cls, var_name, indices, expression): ) return var_value - @classmethod - def _find_stretch_operand(cls, expression): - """Return the name of a 'stretch' variable referenced in ``expression``, if any. - - A ``stretch`` resolves to its span only at schedule time, so its value stays - ``None`` during evaluation. When such an operand reaches arithmetic it cannot be - folded to a compile-time constant, so this locates the stretch variable in order - to report it. - - Args: - expression: The expression (or sub-expression) to inspect. - - Returns: - str | None: The name of a referenced stretch variable, or ``None``. - """ - var_name = None - if isinstance(expression, Identifier): - var_name = expression.name - elif isinstance(expression, IndexExpression): - var_name, _ = Qasm3Analyzer.analyze_index_expression(expression) - elif isinstance(expression, BinaryExpression): - return cls._find_stretch_operand(expression.lhs) or cls._find_stretch_operand( - expression.rhs - ) - elif isinstance(expression, UnaryExpression): - return cls._find_stretch_operand(expression.expression) - - if var_name is not None: - var = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name) - if var is not None and isinstance(var.base_type, StretchType): - return var_name - return None - @classmethod # pylint: disable-next=too-many-return-statements,too-many-branches,too-many-statements,too-many-locals,too-many-arguments def evaluate_expression( # type: ignore[return] @@ -504,11 +471,15 @@ def _get_external_function_return_type(expression): return (None, []) statements.extend(rhs_statements) - if lhs_value is None or rhs_value is None: - stretch_name = cls._find_stretch_operand(expression) - if stretch_name is not None: + 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 '{stretch_name}' is not yet supported", + f"Arithmetic on 'stretch' operand '{operand.name}' is not yet supported", err_type=ValidationError, error_node=expression, span=expression.span, diff --git a/tests/qasm3/declarations/test_classical.py b/tests/qasm3/declarations/test_classical.py index e30b5e73..0c084d51 100644 --- a/tests/qasm3/declarations/test_classical.py +++ b/tests/qasm3/declarations/test_classical.py @@ -542,6 +542,17 @@ def test_stretch_type_error(qasm_code, error_message, error_span, caplog): "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): @@ -554,6 +565,17 @@ def test_stretch_arithmetic_not_supported(qasm_code, stretch_var, error_span, ca 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", [