Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 28 additions & 1 deletion src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]]:
Expand Down
9 changes: 9 additions & 0 deletions tests/qasm3/test_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading