diff --git a/CHANGELOG.md b/CHANGELOG.md index 5135f57..6cf7729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Types of changes: ### Fixed - Fixed `cs` and `csdg` from `stdgates.inc` being rejected during unrolling. They now decompose to controlled phase shifts. ([#439](https://github.com/qBraid/pyqasm/issues/439)) - Fixed Clifford+T rebasing for exact `rx`, `ry`, and `rz` rotations at multiples of π/4. These gates now decompose instead of disappearing, while angles outside the exact basis raise `RebaseError` instead of producing an incorrect result. ([#428](https://github.com/qBraid/pyqasm/issues/428)) +- Fixed a bare expression statement raising an `AttributeError` that escaped the public API. The expression-statement handler assumed every expression was a function call and dereferenced `.name`, so a statement such as `a * b;` (used throughout the spec's classical types examples) crashed with `'BinaryExpression' object has no attribute 'name'`. Such statements have no effect and are now rejected with a `ValidationError` reading `Expression statement has no effect`, carrying the source span. ([#388](https://github.com/qBraid/pyqasm/issues/388)) ### Dependencies diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index a1afb6f..fbafc30 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -196,7 +196,7 @@ def _construct_visit_map(self): qasm3_ast.SwitchStatement: self._visit_switch_statement, qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExternDeclaration: self._visit_subroutine_definition, - qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), + qasm3_ast.ExpressionStatement: self._visit_expression_statement, qasm3_ast.IODeclaration: lambda x: [], qasm3_ast.BreakStatement: self._visit_break, qasm3_ast.ContinueStatement: self._visit_continue, @@ -2618,6 +2618,33 @@ def _visit_subroutine_definition( return statements # pylint: disable=too-many-locals, too-many-statements + def _visit_expression_statement( + self, statement: qasm3_ast.ExpressionStatement + ) -> tuple[Any | None, list[qasm3_ast.Statement | qasm3_ast.FunctionCall]]: + """Visit an expression statement. + + Only a function call has an effect on the program. Any other bare + expression statement, such as the arithmetic examples in the spec's + classical types chapter, has no effect, so reject it with a clear + error instead of dereferencing ``.name`` on an expression that is not + a function call. + + Args: + statement (ExpressionStatement): The expression statement to visit. + + Returns: + tuple[Any | None, list[Statement | FunctionCall]]: The result of + visiting the underlying function call. + """ + expression = statement.expression + if not isinstance(expression, qasm3_ast.FunctionCall): + raise_qasm3_error( + "Expression statement has no effect", + error_node=statement, + span=statement.span, + ) + return self._visit_function_call(expression) + def _visit_function_call( self, statement: qasm3_ast.FunctionCall ) -> tuple[Any | None, list[qasm3_ast.Statement | qasm3_ast.FunctionCall]]: diff --git a/tests/qasm3/test_expressions.py b/tests/qasm3/test_expressions.py index ef549f5..40a295e 100644 --- a/tests/qasm3/test_expressions.py +++ b/tests/qasm3/test_expressions.py @@ -105,3 +105,12 @@ def test_incorrect_expressions(caplog): loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() assert "Error at line 1" in caplog.text assert "x" in caplog.text + + +def test_bare_expression_statement_is_rejected(caplog): + """A bare expression statement has no effect and must raise a clear error.""" + with pytest.raises(ValidationError, match="Expression statement has no effect"): + with caplog.at_level("ERROR"): + loads("OPENQASM 3; int[32] a = 2; int[32] b = 3; a * b;").validate() + assert "Error at line 1" in caplog.text + assert "a * b" in caplog.text