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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ Types of changes:
### Added

### Improved / Modified
- `raise_qasm3_error` is now annotated `NoReturn`. Every path through it raises, but the `None` return type made each caller look like it could fall through, so eight `# type: ignore[return]` comments and `inconsistent-return-statements` suppressions existed only to silence that. They are gone, and `mypy` and `pylint` now check those functions instead of skipping them.

### Deprecated

### Removed

### Fixed
- Fixed a `switch` whose target matches no case and which declares no `default` crashing with `TypeError: 'NoneType' object is not iterable`. The spec does not require a `default`, so such a switch is valid and now contributes no statements. `_visit_switch_statement` fell off its last branch and returned `None`, which a `# type: ignore[return]` had been hiding.
- Fixed `pyqasm validate` wrapping its diagnostics at the console width, which split a file path longer than the width across lines mid-token and left it neither copyable nor clickable. The error console now uses `soft_wrap`, keeping one diagnostic per line.
- Fixed an indirect cycle between gate definitions exhausting the Python stack: `gate a q { b q; }` with `gate b q { a q; }` raised a bare `RecursionError` naming nothing, while the direct case was already reported cleanly. The guard compared the body's gate name against one name, so it saw only a cycle of length one. It now tests membership of the whole expansion chain, and names the path: `Recursive definitions not allowed for gate 'a' (a -> b -> a)`. A gate reached twice down separate paths is a diamond, not a cycle, and still expands. ([#369](https://github.com/qBraid/pyqasm/issues/369))
- Fixed a nested external custom gate counting the depth of the decomposition it skipped, the shape the [#352](https://github.com/qBraid/pyqasm/issues/352) fix did not reach: `unroll(external_gates=["outer"])` on a gate whose body calls another custom gate emitted one statement but reported `depth() == 13`. The suppression flag was assigned and cleared without save-restore, so the inner gate clobbered the outer gate's state in both directions. It is now saved and restored, and the depth is recorded once, from the outermost external gate. ([#367](https://github.com/qBraid/pyqasm/issues/367))
Expand Down
4 changes: 2 additions & 2 deletions src/pyqasm/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ def get_op_bit_list(operation):
)
return bit_list

@staticmethod # pylint: disable-next=inconsistent-return-statements
def extract_qasm_version(qasm: str) -> float: # type: ignore[return]
@staticmethod
def extract_qasm_version(qasm: str) -> float:
"""
Extracts the OpenQASM version from a given OpenQASM string.

Expand Down
4 changes: 2 additions & 2 deletions src/pyqasm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import os
import sys
from typing import Optional, Type
from typing import NoReturn, Optional, Type

from openqasm3.ast import QASMNode, Span
from openqasm3.parser import QASM3ParsingError
Expand Down Expand Up @@ -89,7 +89,7 @@ def raise_qasm3_error(
error_node: Optional[QASMNode] = None,
span: Optional[Span] = None,
raised_from: Optional[Exception] = None,
) -> None:
) -> NoReturn:
"""Raises a QASM3 conversion error with optional chaining from another exception.

Args:
Expand Down
2 changes: 1 addition & 1 deletion src/pyqasm/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def _get_var_value(cls, var_name, indices, expression):

@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]
def evaluate_expression(
cls,
expression,
const_expr: bool = False,
Expand Down
5 changes: 4 additions & 1 deletion src/pyqasm/maps/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def qasm3_expression_op_map(op_name: str, *args) -> float | int | bool:
raise ValidationError(f"Unsupported / undeclared QASM operator: {op_name}") from exc


# pylint: disable=inconsistent-return-statements,too-many-return-statements
# pylint: disable=too-many-return-statements
def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value):
"""Cast the variable type to the type to match, if possible.

Expand Down Expand Up @@ -133,6 +133,9 @@ def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value):
if isinstance(rhs_value, float):
return complex(rhs_value)
return rhs_value
raise ValidationError(
f"Unsupported cast to type '{openqasm_type.__name__}' for variable '{var_name}'"
)


# IEEE 754 Standard for floats
Expand Down
3 changes: 1 addition & 2 deletions src/pyqasm/maps/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,8 +1166,7 @@ def map_qasm_op_num_params(op_name: str) -> int:
return 0


# pylint: disable-next=inconsistent-return-statements
def map_qasm_op_to_callable(op_node: QuantumGate) -> tuple[Callable, int]: # type: ignore[return]
def map_qasm_op_to_callable(op_node: QuantumGate) -> tuple[Callable, int]:
"""
Map a QASM operation to a callable.

Expand Down
51 changes: 27 additions & 24 deletions src/pyqasm/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def validate_gate_call(
)

@staticmethod
def validate_return_statement( # pylint: disable=inconsistent-return-statements
def validate_return_statement(
subroutine_def: SubroutineDefinition,
return_statement: ReturnStatement,
return_value: Any,
Expand All @@ -317,27 +317,30 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements
error_node=return_statement,
span=return_statement.span,
)
else:
if return_value is None:
raise_qasm3_error(
f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
f" Expected {type(subroutine_def.return_type)} but got void",
error_node=return_statement,
span=return_statement.span,
)
base_size = 1
if hasattr(subroutine_def.return_type, "size"):
base_size = subroutine_def.return_type.size.value

return Qasm3Validator.validate_variable_assignment_value(
Variable(
subroutine_def.name.name + "_return",
subroutine_def.return_type,
base_size,
None,
None,
span=return_statement.span,
),
return_value,
op_node=return_statement,
# A void subroutine carries no value back to its caller.
return None

if return_value is None:
raise_qasm3_error(
f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
f" Expected {type(subroutine_def.return_type)} but got void",
error_node=return_statement,
span=return_statement.span,
)

base_size = 1
if hasattr(subroutine_def.return_type, "size"):
base_size = subroutine_def.return_type.size.value

return Qasm3Validator.validate_variable_assignment_value(
Variable(
subroutine_def.name.name + "_return",
subroutine_def.return_type,
base_size,
None,
None,
span=return_statement.span,
),
return_value,
op_node=return_statement,
)
9 changes: 5 additions & 4 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2892,7 +2892,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No

return []

def _visit_switch_statement( # type: ignore[return]
def _visit_switch_statement(
self, statement: qasm3_ast.SwitchStatement
) -> list[qasm3_ast.Statement]:
"""Visit a switch statement element.
Expand Down Expand Up @@ -2979,9 +2979,10 @@ def _evaluate_case(statements):
case_stmts = case[1].statements
return _evaluate_case(case_stmts)

if not case_fulfilled and statement.default:
default_stmts = statement.default.statements
return _evaluate_case(default_stmts)
# Reaching here means no case matched: the loop returns as soon as one does.
if statement.default:
return _evaluate_case(statement.default.statements)
return []

def _resolve_duration_unit(self, time_var: qasm3_ast.Expression) -> qasm3_ast.TimeUnit:
"""Determine the output unit for a duration literal.
Expand Down
33 changes: 33 additions & 0 deletions tests/qasm3/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,39 @@ def test_switch_const_int():
check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x")


def test_switch_no_match_without_default():
"""Test a switch whose target matches no case and which has no default block.

The spec does not require a default, so this is valid and must simply contribute
no statements. It previously raised ``TypeError: 'NoneType' object is not iterable``.
"""

qasm3_switch_program = """
OPENQASM 3.0;
include "stdgates.inc";

int i = 5;
qubit q;

switch(i) {
case 1 {
x q;
}
case 2 {
z q;
}
}
h q;
"""

result = loads(qasm3_switch_program)
result.unroll()

assert result.num_qubits == 1
# Only the gate after the switch survives; neither case body is emitted.
check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h")


def test_switch_duplicate_cases():
"""Test that switch raises error if duplicate values are present in case."""

Expand Down
Loading