From 8a00ed7ccc97e231ad9f457976a4d3b88ab193dc Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 3 Apr 2025 11:57:26 +0530 Subject: [PATCH 01/16] fix stage 1 --- src/pyqasm/visitor.py | 20 ++++++++++++++------ tests/qasm3/resources/gates.py | 6 +++--- tests/qasm3/test_alias.py | 2 +- tests/qasm3/test_barrier.py | 4 +++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 1365aa9..c5b53cd 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -334,8 +334,12 @@ def _check_if_name_in_scope(self, name: str, operation: Any) -> None: for scope_level in range(0, self._curr_scope + 1): if name in self._label_scope_level[scope_level]: return + + operation_type = type(operation).__name__ + operation_name = operation.name.name if hasattr(operation.name, "name") else operation.name raise_qasm3_error( - f"Variable {name} not in scope for operation {operation}", span=operation.span + f"Variable '{name}' not in scope for {operation_type} '{operation_name}'", + span=operation.span, ) # pylint: disable-next=too-many-locals,too-many-branches @@ -390,8 +394,12 @@ def _get_op_bits( replace_alias = True reg_size_map = self._global_alias_size_map else: + err_msg = ( + f"Missing {'qubit' if qubits else 'clbit'} register declaration " + f"for '{reg_name}' in {type(operation).__name__}" + ) raise_qasm3_error( - f"Missing register declaration for {reg_name} in operation {operation}", + err_msg, span=operation.span, ) self._check_if_name_in_scope(reg_name, operation) @@ -639,7 +647,7 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> """ gate_name = definition.name.name if gate_name in self._custom_gates: - raise_qasm3_error(f"Duplicate gate definition for {gate_name}", span=definition.span) + raise_qasm3_error(f"Duplicate quantum gate definition for '{gate_name}'", span=definition.span) self._custom_gates[gate_name] = definition return [] @@ -877,7 +885,7 @@ def _visit_custom_gate_operation( # in case the gate is reapplied if isinstance(gate_op, qasm3_ast.QuantumGate) and gate_op.name.name == gate_name: raise_qasm3_error( - f"Recursive definitions not allowed for gate {gate_name}", span=gate_op.span + f"Recursive definitions not allowed for gate '{gate_name}'", span=gate_op.span ) Qasm3Transformer.transform_gate_params(gate_op_copy, param_map) Qasm3Transformer.transform_gate_qubits(gate_op_copy, qubit_map) @@ -891,7 +899,7 @@ def _visit_custom_gate_operation( else: # TODO: add control flow support raise_qasm3_error( - f"Unsupported gate definition statement {gate_op}", span=gate_op.span + f"Unsupported statement in gate definition '{type(gate_op).__name__}'", span=gate_op.span ) self._restore_context() @@ -1025,7 +1033,7 @@ def _visit_phase_operation( # if args are provided in global scope, then we should raise error if self._in_global_scope() and len(operation.qubits) != 0: raise_qasm3_error( - f"Qubit arguments not allowed for phase operation {str(operation)} in global scope", + f"Qubit arguments not allowed for 'gphase' operation in global scope", span=operation.span, ) diff --git a/tests/qasm3/resources/gates.py b/tests/qasm3/resources/gates.py index f15d36d..9e7306d 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -255,7 +255,7 @@ def test_fixture(): qubit[2] q1; h q2; // undeclared register """, - "Missing register declaration for q2 .*", + "Missing qubit register declaration for 'q2' in QuantumGate", ), "undeclared_1qubit_op": ( """ @@ -328,7 +328,7 @@ def test_fixture(): qubit q; gphase(pi) q; """, - r"Qubit arguments not allowed for phase operation", + r"Qubit arguments not allowed for 'gphase' operation", ), "undeclared_custom": ( """ @@ -431,6 +431,6 @@ def test_fixture(): qubit[2] q1; custom_gate(0.5, 0.5) q1; // duplicate definition """, - "Duplicate gate definition for custom_gate", + "Duplicate quantum gate definition for 'custom_gate'", ), } diff --git a/tests/qasm3/test_alias.py b/tests/qasm3/test_alias.py index 669037d..e3d7354 100644 --- a/tests/qasm3/test_alias.py +++ b/tests/qasm3/test_alias.py @@ -283,7 +283,7 @@ def test_alias_out_of_scope(): """Test converting OpenQASM 3 program with alias out of scope.""" with pytest.raises( ValidationError, - match=r"Variable alias not in scope for operation .*", + match="Variable 'alias' not in scope for QuantumGate 'cx'", ): qasm3_alias_program = """ OPENQASM 3; diff --git a/tests/qasm3/test_barrier.py b/tests/qasm3/test_barrier.py index 7d122bd..942a292 100644 --- a/tests/qasm3/test_barrier.py +++ b/tests/qasm3/test_barrier.py @@ -165,7 +165,9 @@ def test_incorrect_barrier(): barrier q2; """ - with pytest.raises(ValidationError, match=r"Missing register declaration for q2 .*"): + with pytest.raises( + ValidationError, match="Missing qubit register declaration for 'q2' in QuantumBarrier" + ): loads(undeclared).validate() out_of_bounds = """ From 3c341760f92c1a3da3ff679cc268de0c3c93c9c5 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Apr 2025 16:12:40 +0530 Subject: [PATCH 02/16] fix errors 2 --- src/pyqasm/analyzer.py | 8 ++++---- src/pyqasm/expressions.py | 25 ++++++++++++------------ src/pyqasm/subroutines.py | 2 +- src/pyqasm/transformer.py | 5 +++-- src/pyqasm/validator.py | 20 ++++++++++--------- src/pyqasm/visitor.py | 18 ++++++++++------- tests/qasm3/declarations/test_quantum.py | 4 ++-- tests/qasm3/resources/gates.py | 8 ++++---- tests/qasm3/resources/subroutines.py | 2 +- tests/qasm3/resources/variables.py | 22 ++++++++++----------- tests/qasm3/test_expressions.py | 4 ++-- tests/qasm3/test_if.py | 2 +- tests/qasm3/test_sizeof.py | 4 ++-- tests/qasm3/test_switch.py | 6 ++++-- 14 files changed, 69 insertions(+), 61 deletions(-) diff --git a/src/pyqasm/analyzer.py b/src/pyqasm/analyzer.py index 3ce6ff7..57ed631 100644 --- a/src/pyqasm/analyzer.py +++ b/src/pyqasm/analyzer.py @@ -87,7 +87,7 @@ def _validate_index(index, dimension, var_name, span, dim_num): if index < 0 or index >= dimension: raise_qasm3_error( message=f"Index {index} out of bounds for dimension {dim_num} " - f"of variable {var_name}", + f"of variable '{var_name}'. Expected index in range [0, {dimension-1}]", err_type=ValidationError, span=span, ) @@ -105,8 +105,8 @@ def _validate_step(start_id, end_id, step, span): for i, index in enumerate(indices): if not isinstance(index, (Identifier, Expression, RangeDefinition, IntegerLiteral)): raise_qasm3_error( - message=f"Unsupported index type {type(index)} for " - f"classical variable {var.name}", + message=f"Unsupported index type '{type(index)}' for " + f"classical variable '{var.name}'", err_type=ValidationError, span=index.span, ) @@ -283,6 +283,6 @@ def verify_gate_qubits(gate: QuantumGate, span: Optional[Span] = None): if duplicate_qubit: qubit_name, qubit_id = duplicate_qubit raise_qasm3_error( - f"Duplicate qubit {qubit_name}[{qubit_id}] in gate {gate.name.name}", + f"Duplicate qubit '{qubit_name}[{qubit_id}]' arg in gate {gate.name.name}", span=span, ) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 63e6626..6b9985c 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -67,7 +67,7 @@ def _check_var_in_scope(cls, var_name, expression): if not cls.visitor_obj._check_in_scope(var_name): raise_qasm3_error( - f"Undefined identifier {var_name} in expression", + f"Undefined identifier '{var_name}' in expression", ValidationError, expression.span, ) @@ -88,7 +88,7 @@ def _check_var_constant(cls, var_name, const_expr, expression): const_var = cls.visitor_obj._get_from_visible_scope(var_name).is_constant if const_expr and not const_var: raise_qasm3_error( - f"Variable '{var_name}' is not a constant in given expression", + f"Expected variable '{var_name}' to be constant in given expression", ValidationError, expression.span, ) @@ -106,12 +106,11 @@ def _check_var_type(cls, var_name, reqd_type, expression): Raises: ValidationError: If the variable has an invalid type for the required type. """ - - if not Qasm3Validator.validate_variable_type( - cls.visitor_obj._get_from_visible_scope(var_name), reqd_type - ): + var = cls.visitor_obj._get_from_visible_scope(var_name) + if not Qasm3Validator.validate_variable_type(var, reqd_type): raise_qasm3_error( - f"Invalid type of variable {var_name} for required type {reqd_type}", + f"Invalid type '{var.base_type}' of variable '{var_name}' for " + f"required type {reqd_type}", ValidationError, expression.span, ) @@ -130,7 +129,7 @@ def _check_var_initialized(var_name, var_value, expression): if var_value is None: raise_qasm3_error( - f"Uninitialized variable {var_name} in expression", + f"Uninitialized variable '{var_name}' in expression", ValidationError, expression.span, ) @@ -207,7 +206,7 @@ def _process_variable(var_name: str, indices=None): if not reqd_type or reqd_type == Qasm3FloatType: return _check_and_return_value(CONSTANTS_MAP[var_name]) raise_qasm3_error( - f"Constant {var_name} not allowed in non-float expression", + f"Constant '{var_name}' not allowed in non-float expression", ValidationError, expression.span, ) @@ -229,14 +228,14 @@ def _process_variable(var_name: str, indices=None): ).dims else: raise_qasm3_error( - message=f"Unsupported target type {type(target)} for sizeof expression", + message=f"Unsupported target type '{type(target)}' for sizeof expression", err_type=ValidationError, span=expression.span, ) if dimensions is None or len(dimensions) == 0: raise_qasm3_error( - message=f"Invalid sizeof usage, variable {var_name} is not an array.", + message=f"Invalid sizeof usage, variable '{var_name}' is not an array.", err_type=ValidationError, span=expression.span, ) @@ -250,7 +249,7 @@ def _process_variable(var_name: str, indices=None): assert index is not None and isinstance(index, int) if index < 0 or index >= len(dimensions): raise_qasm3_error( - f"Index {index} out of bounds for array {var_name} with " + f"Index {index} out of bounds for array '{var_name}' with " f"{len(dimensions)} dimensions", ValidationError, expression.span, @@ -279,7 +278,7 @@ def _process_variable(var_name: str, indices=None): ) if expression.op.name == "~" and not isinstance(operand, int): raise_qasm3_error( - f"Unsupported expression type {type(operand)} in ~ operation", + f"Unsupported expression type '{type(operand)}' in ~ operation", ValidationError, expression.span, ) diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index 34e0f54..9d9acee 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -181,7 +181,7 @@ def _process_classical_arg_by_reference( if actual_arg_name is None: raise_qasm3_error( array_expected_type_msg - + f"Literal {Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]} " + + f"Literal '{Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]}' " + "found in function call", span=span, ) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 039dbaa..36ba5ab 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -111,7 +111,7 @@ def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: for value in discrete_set.values: if not isinstance(value, IntegerLiteral): raise_qasm3_error( - f"Unsupported discrete set value {value} in discrete set", + f"Unsupported discrete set value '{value}' in discrete set", span=discrete_set.span, ) values.append(value.value) @@ -167,7 +167,8 @@ def transform_gate_qubits( for i, qubit in enumerate(gate_op.qubits): if isinstance(qubit, IndexedIdentifier): raise_qasm3_error( - f"Indexing '{qubit.name.name}' not supported in gate definition", + f"Indexing '{qubit.name.name}' not supported in gate definition " + f"for gate {gate_op.name}", span=qubit.span, ) gate_qubit_name = qubit.name diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index 048e275..7ae35fe 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -67,7 +67,7 @@ def validate_statement_type(blacklisted_stmts: set, statement: Any, construct: s if stmt_type in blacklisted_stmts: if stmt_type != ClassicalDeclaration: raise_qasm3_error( - f"Unsupported statement {stmt_type} in {construct} block", + f"Unsupported statement '{stmt_type}' in {construct} block", span=statement.span, ) @@ -116,7 +116,9 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any: try: type_to_match = VARIABLE_TYPE_MAP[qasm_type] except KeyError as err: - raise ValidationError(f"Invalid type {qasm_type} for variable {variable.name}") from err + raise ValidationError( + f"Invalid type {qasm_type} for variable '{variable.name}'" + ) from err # For each type we will have a "castable" type set and its corresponding cast operation type_casted_value = qasm_variable_type_cast(qasm_type, variable.name, base_size, value) @@ -150,14 +152,14 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any: if type_casted_value < left or type_casted_value > right: raise_qasm3_error( - f"Value {value} out of limits for variable {variable.name} with " + f"Value {value} out of limits for variable '{variable.name}' with " f"base size {base_size}", ) elif type_to_match == bool: pass else: raise_qasm3_error( - f"Invalid type {type_to_match} for variable {variable.name}", TypeError + f"Invalid type {type_to_match} for variable '{variable.name}'", TypeError ) return type_casted_value @@ -176,11 +178,11 @@ def validate_classical_type(base_type, base_size, var_name, span) -> None: ValidationError: If the type or size is invalid. """ if not isinstance(base_size, int) or base_size <= 0: - raise_qasm3_error(f"Invalid base size {base_size} for variable {var_name}", span=span) + raise_qasm3_error(f"Invalid base size {base_size} for variable '{var_name}'", span=span) if isinstance(base_type, FloatType) and base_size not in [32, 64]: raise_qasm3_error( - f"Invalid base size {base_size} for float variable {var_name}", span=span + f"Invalid base size {base_size} for float variable '{var_name}'", span=span ) @staticmethod @@ -200,7 +202,7 @@ def validate_array_assignment_values( # recursively check the array if values.shape[0] != dimensions[0]: raise_qasm3_error( - f"Invalid dimensions for array assignment to variable {variable.name}. " + f"Invalid dimensions for array assignment to variable '{variable.name}'. " f"Expected {dimensions[0]} but got {values.shape[0]}", ) for i, value in enumerate(values): @@ -235,7 +237,7 @@ def validate_gate_call( if op_num_args != gate_def_num_args: s = "" if gate_def_num_args == 1 else "s" raise_qasm3_error( - f"Parameter count mismatch for gate {operation.name.name}: " + f"Parameter count mismatch for gate '{operation.name.name}': " f"expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.", span=operation.span, ) @@ -244,7 +246,7 @@ def validate_gate_call( if qubits_in_op != gate_def_num_qubits: s = "" if gate_def_num_qubits == 1 else "s" raise_qasm3_error( - f"Qubit count mismatch for gate {operation.name.name}: " + f"Qubit count mismatch for gate '{operation.name.name}': " f"expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.", span=operation.span, ) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c5b53cd..86cc371 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -647,7 +647,9 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> """ gate_name = definition.name.name if gate_name in self._custom_gates: - raise_qasm3_error(f"Duplicate quantum gate definition for '{gate_name}'", span=definition.span) + raise_qasm3_error( + f"Duplicate quantum gate definition for '{gate_name}'", span=definition.span + ) self._custom_gates[gate_name] = definition return [] @@ -885,7 +887,8 @@ def _visit_custom_gate_operation( # in case the gate is reapplied if isinstance(gate_op, qasm3_ast.QuantumGate) and gate_op.name.name == gate_name: raise_qasm3_error( - f"Recursive definitions not allowed for gate '{gate_name}'", span=gate_op.span + f"Recursive definitions not allowed for gate '{gate_name}'", + span=gate_op.span, ) Qasm3Transformer.transform_gate_params(gate_op_copy, param_map) Qasm3Transformer.transform_gate_qubits(gate_op_copy, qubit_map) @@ -899,7 +902,8 @@ def _visit_custom_gate_operation( else: # TODO: add control flow support raise_qasm3_error( - f"Unsupported statement in gate definition '{type(gate_op).__name__}'", span=gate_op.span + f"Unsupported statement in gate definition '{type(gate_op).__name__}'", + span=gate_op.span, ) self._restore_context() @@ -1033,7 +1037,7 @@ def _visit_phase_operation( # if args are provided in global scope, then we should raise error if self._in_global_scope() and len(operation.qubits) != 0: raise_qasm3_error( - f"Qubit arguments not allowed for 'gphase' operation in global scope", + "Qubit arguments not allowed for 'gphase' operation in global scope", span=operation.span, ) @@ -1205,7 +1209,7 @@ def _visit_constant_declaration( ] if not isinstance(base_size, int) or base_size <= 0: raise_qasm3_error( - f"Invalid base size {base_size} for variable {var_name}", + f"Invalid base size {base_size} for variable '{var_name}'", span=statement.span, ) @@ -1281,7 +1285,7 @@ def _visit_classical_declaration( ) if len(dimensions) > MAX_ARRAY_DIMENSIONS: raise_qasm3_error( - f"Invalid dimensions {len(dimensions)} for array declaration for {var_name}. " + f"Invalid dimensions {len(dimensions)} for array declaration for '{var_name}'. " f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}", span=statement.span, ) @@ -1290,7 +1294,7 @@ def _visit_classical_declaration( dim_value = Qasm3ExprEvaluator.evaluate_expression(dim, const_expr=True)[0] if not isinstance(dim_value, int) or dim_value <= 0: raise_qasm3_error( - f"Invalid dimension size {dim_value} in array declaration for {var_name}", + f"Invalid dimension size {dim_value} in array declaration for '{var_name}'", span=statement.span, ) final_dimensions.append(dim_value) diff --git a/tests/qasm3/declarations/test_quantum.py b/tests/qasm3/declarations/test_quantum.py index 7c82b33..7fa9508 100644 --- a/tests/qasm3/declarations/test_quantum.py +++ b/tests/qasm3/declarations/test_quantum.py @@ -165,7 +165,7 @@ def test_clbit_redeclaration_error(): def test_non_constant_size(): """Test non-constant size in qubit and clbit declarations""" with pytest.raises( - ValidationError, match=r"Variable 'N' is not a constant in given expression" + ValidationError, match=r"Expected variable 'N' to be constant in given expression" ): qasm3_string = """ OPENQASM 3.0; @@ -176,7 +176,7 @@ def test_non_constant_size(): loads(qasm3_string).validate() with pytest.raises( - ValidationError, match=r"Variable 'size' is not a constant in given expression" + ValidationError, match=r"Expected variable 'size' to be constant in given expression" ): qasm3_string = """ OPENQASM 3.0; diff --git a/tests/qasm3/resources/gates.py b/tests/qasm3/resources/gates.py index 9e7306d..a538922 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -306,7 +306,7 @@ def test_fixture(): qubit[2] q1; rx(a) q1; // unsupported parameter type """, - "Undefined identifier a in.*", + "Undefined identifier 'a' in.*", ), "duplicate_qubits": ( """ @@ -316,7 +316,7 @@ def test_fixture(): qubit[2] q1; cx q1[0] , q1[0]; // duplicate qubit """, - r"Duplicate qubit q1\[0\] in gate cx", + r"Duplicate qubit 'q1\[0\]' arg in gate cx", ), } @@ -353,7 +353,7 @@ def test_fixture(): qubit[2] q1; custom_gate(0.5) q1; // parameter count mismatch """, - "Parameter count mismatch for gate custom_gate: expected 2 arguments, but got 1 instead.", + "Parameter count mismatch for gate 'custom_gate': expected 2 arguments, but got 1 instead.", ), "parameter_mismatch_2": ( """ @@ -382,7 +382,7 @@ def test_fixture(): qubit[3] q1; custom_gate(0.5, 0.5) q1; // qubit count mismatch """, - "Qubit count mismatch for gate custom_gate: expected 2 qubits, but got 3 instead.", + "Qubit count mismatch for gate 'custom_gate': expected 2 qubits, but got 3 instead.", ), "indexing_not_supported": ( """ diff --git a/tests/qasm3/resources/subroutines.py b/tests/qasm3/resources/subroutines.py index 83939ea..9ce8b80 100644 --- a/tests/qasm3/resources/subroutines.py +++ b/tests/qasm3/resources/subroutines.py @@ -271,7 +271,7 @@ def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { my_function(q, 5); """, r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." - r" Literal 5 found in function call", + r" Literal '5' found in function call", ), "type_mismatch_in_array": ( """ diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index e0c31a8..b2c7d1c 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -77,7 +77,7 @@ include "stdgates.inc"; int[32.1] x; """, - "Invalid base size 32.1 for variable x", + "Invalid base size 32.1 for variable 'x'", ), "invalid_const_int_size": ( """ @@ -85,7 +85,7 @@ include "stdgates.inc"; const int[32.1] x = 3; """, - "Invalid base size 32.1 for variable x", + "Invalid base size 32.1 for variable 'x'", ), "const_declaration_with_non_const": ( """ @@ -94,7 +94,7 @@ int[32] x = 5; const int[32] y = x + 5; """, - "Variable 'x' is not a constant in given expression", + "Expected variable 'x' to be constant in given expression", ), "const_declaration_with_non_const_size": ( """ @@ -103,7 +103,7 @@ int[32] x = 5; const int[x] y = 5; """, - "Variable 'x' is not a constant in given expression", + "Expected variable 'x' to be constant in given expression", ), "invalid_float_size": ( """ @@ -112,7 +112,7 @@ float[23] x; """, - "Invalid base size 23 for float variable x", + "Invalid base size 23 for float variable 'x'", ), "unsupported_types": ( """ @@ -121,7 +121,7 @@ angle x = 3.4; """, - "Invalid type for variable x", + "Invalid type for variable 'x'", ), "imaginary_variable": ( """ @@ -139,7 +139,7 @@ array[int[32], 1, 2.1] x; """, - "Invalid dimension size 2.1 in array declaration for x", + "Invalid dimension size 2.1 in array declaration for 'x'", ), "extra_array_dimensions": ( """ @@ -148,7 +148,7 @@ array[int[32], 1, 2, 3, 4, 5, 6, 7, 8] x; """, - "Invalid dimensions 8 for array declaration for x. Max allowed dimensions is 7", + "Invalid dimensions 8 for array declaration for 'x'. Max allowed dimensions is 7", ), "dimension_mismatch_1": ( """ @@ -157,7 +157,7 @@ array[int[32], 1, 2] x = {1,2,3}; """, - "Invalid dimensions for array assignment to variable x. Expected 1 but got 3", + "Invalid dimensions for array assignment to variable 'x'. Expected 1 but got 3", ), "dimension_mismatch_2": ( """ @@ -240,7 +240,7 @@ float[32] x = 123456789123456789123456789123456789123456789.1; """, - "Value .* out of limits for variable x with base size 32", + "Value .* out of limits for variable 'x' with base size 32", ), "indexing_non_array": ( """ @@ -281,6 +281,6 @@ array[int[32], 3] x; x[3] = 3; """, - "Index 3 out of bounds for dimension 0 of variable x", + "Index 3 out of bounds for dimension 0 of variable 'x'", ), } diff --git a/tests/qasm3/test_expressions.py b/tests/qasm3/test_expressions.py index aa9bca1..88ce351 100644 --- a/tests/qasm3/test_expressions.py +++ b/tests/qasm3/test_expressions.py @@ -84,8 +84,8 @@ def test_incorrect_expressions(): with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): loads("OPENQASM 3; qubit q; rx(~1.3+5im) q;").validate() - with pytest.raises(ValidationError, match="Undefined identifier x in expression"): + with pytest.raises(ValidationError, match="Undefined identifier 'x' in expression"): loads("OPENQASM 3; qubit q; rx(x) q;").validate() - with pytest.raises(ValidationError, match="Uninitialized variable x in expression"): + with pytest.raises(ValidationError, match="Uninitialized variable 'x' in expression"): loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() diff --git a/tests/qasm3/test_if.py b/tests/qasm3/test_if.py index 8e57fe2..e3eeec8 100644 --- a/tests/qasm3/test_if.py +++ b/tests/qasm3/test_if.py @@ -224,7 +224,7 @@ def test_incorrect_if(): """ ).validate() - with pytest.raises(ValidationError, match=r"Undefined identifier c2 in expression"): + with pytest.raises(ValidationError, match=r"Undefined identifier 'c2' in expression"): loads( """ OPENQASM 3.0; diff --git a/tests/qasm3/test_sizeof.py b/tests/qasm3/test_sizeof.py index 04f7091..fc7b8a6 100644 --- a/tests/qasm3/test_sizeof.py +++ b/tests/qasm3/test_sizeof.py @@ -96,7 +96,7 @@ def test_unsupported_target(): def test_sizeof_on_non_array(): """Test sizeof on a non-array""" with pytest.raises( - ValidationError, match="Invalid sizeof usage, variable my_int is not an array." + ValidationError, match="Invalid sizeof usage, variable 'my_int' is not an array." ): qasm3_string = """ OPENQASM 3; @@ -112,7 +112,7 @@ def test_sizeof_on_non_array(): def test_out_of_bounds_reference(): """Test sizeof on an out of bounds reference""" with pytest.raises( - ValidationError, match="Index 3 out of bounds for array my_ints with 2 dimensions" + ValidationError, match="Index 3 out of bounds for array 'my_ints' with 2 dimensions" ): qasm3_string = """ OPENQASM 3; diff --git a/tests/qasm3/test_switch.py b/tests/qasm3/test_switch.py index d925d30..71b9219 100644 --- a/tests/qasm3/test_switch.py +++ b/tests/qasm3/test_switch.py @@ -416,7 +416,7 @@ def test_non_int_variable_expression(): """ with pytest.raises( ValidationError, - match=r"Invalid type of variable .* for required type ", + match=r"Invalid type .* of variable 'f' for required type ", ): qasm3_switch_program = base_invalid_program loads(qasm3_switch_program).validate() @@ -443,6 +443,8 @@ def test_non_constant_expression_case(): } """ - with pytest.raises(ValidationError, match=r"Variable .* is not a constant in given expression"): + with pytest.raises( + ValidationError, match=r"Expected variable .* to be constant in given expression" + ): qasm3_switch_program = base_invalid_program loads(qasm3_switch_program).validate() From b9c01f0873270f16173cb8957cf0c594a075bd75 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 14 Apr 2025 15:46:26 +0530 Subject: [PATCH 03/16] collapse traceback and add line reporting --- src/pyqasm/analyzer.py | 8 +- src/pyqasm/exceptions.py | 34 +++++++- src/pyqasm/expressions.py | 54 ++++++++----- src/pyqasm/modules/base.py | 9 ++- src/pyqasm/subroutines.py | 15 ++++ src/pyqasm/transformer.py | 7 ++ src/pyqasm/validator.py | 8 +- src/pyqasm/visitor.py | 121 +++++++++++++++++++++++------ tests/qasm3/resources/variables.py | 2 +- tests/qasm3/test_measurement.py | 4 +- 10 files changed, 208 insertions(+), 54 deletions(-) diff --git a/src/pyqasm/analyzer.py b/src/pyqasm/analyzer.py index 57ed631..9388d6e 100644 --- a/src/pyqasm/analyzer.py +++ b/src/pyqasm/analyzer.py @@ -53,7 +53,7 @@ def analyze_classical_indices( """Validate the indices for a classical variable. Args: - indices (list[list[Any]]): The indices to validate. + indices (list[Any]): The indices to validate. var (Variable): The variable to verify Raises: @@ -70,6 +70,7 @@ def analyze_classical_indices( raise_qasm3_error( message=f"Indexing error. Variable {var.name} is not an array", err_type=ValidationError, + error_node=indices[0], span=indices[0].span, ) if isinstance(indices, DiscreteSet): @@ -80,6 +81,7 @@ def analyze_classical_indices( message=f"Invalid number of indices for variable {var.name}. " f"Expected {len(var_dimensions)} but got {len(indices)}", # type: ignore[arg-type] err_type=ValidationError, + error_node=indices[0], span=indices[0].span, ) @@ -89,6 +91,7 @@ def _validate_index(index, dimension, var_name, span, dim_num): message=f"Index {index} out of bounds for dimension {dim_num} " f"of variable '{var_name}'. Expected index in range [0, {dimension-1}]", err_type=ValidationError, + error_node=index, span=span, ) @@ -99,6 +102,7 @@ def _validate_step(start_id, end_id, step, span): message=f"Index {start_id} is {direction} {end_id} but step" f" is {'negative' if step < 0 else 'positive'}", err_type=ValidationError, + error_node=indices, span=span, ) @@ -108,6 +112,7 @@ def _validate_step(start_id, end_id, step, span): message=f"Unsupported index type '{type(index)}' for " f"classical variable '{var.name}'", err_type=ValidationError, + error_node=index, span=index.span, ) @@ -284,5 +289,6 @@ def verify_gate_qubits(gate: QuantumGate, span: Optional[Span] = None): qubit_name, qubit_id = duplicate_qubit raise_qasm3_error( f"Duplicate qubit '{qubit_name}[{qubit_id}]' arg in gate {gate.name.name}", + error_node=gate, span=span, ) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 119a43c..994c810 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -18,10 +18,21 @@ """ import logging +import os +import sys from typing import Optional, Type -from openqasm3.ast import Span +from openqasm3.ast import QASMNode, Span from openqasm3.parser import QASM3ParsingError +from openqasm3.printer import dumps + +# Define a custom logger for the module +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s")) + +logger = logging.getLogger("pyqasm") +logger.addHandler(handler) +logger.setLevel(logging.WARNING) class PyQasmError(Exception): @@ -48,6 +59,7 @@ class QasmParsingError(QASM3ParsingError): def raise_qasm3_error( message: Optional[str] = None, err_type: Type[Exception] = ValidationError, + error_node: Optional[QASMNode] = None, span: Optional[Span] = None, raised_from: Optional[Exception] = None, ) -> None: @@ -56,17 +68,33 @@ def raise_qasm3_error( Args: message: The error message. If not provided, a default message will be used. err_type: The type of error to raise. + error_node: The QASM node that caused the error. span: The span (location) in the QASM file where the error occurred. raised_from: Optional exception from which this error was raised (chaining). Raises: err_type: The error type initialized with the specified message and chained exception. """ + error_parts = [] + if span: - logging.error( - "Error at line %s, column %s in QASM file", span.start_line, span.start_column + error_parts.append( + f"Error at line {span.start_line}, column {span.start_column} in QASM file" ) + if error_node: + try: + error_parts.append("\n >>>>>> " + dumps(error_node, indent=" ")) + except Exception as _: # pylint: disable = broad-exception-caught + error_parts.append("\n >>>>>> " + str(error_node)) + + if error_parts: + logger.error("\n".join(error_parts)) + + if os.environ.get("PYQASM_EXPAND_TRACEBACK") == "0": + sys.tracebacklimit = 0 # Disable traceback for cleaner output + + # Extract the latest message from the traceback if raised_from is provided if raised_from: raise err_type(message) from raised_from raise err_type(message) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 6b9985c..87e2a6b 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -68,8 +68,9 @@ def _check_var_in_scope(cls, var_name, expression): if not cls.visitor_obj._check_in_scope(var_name): raise_qasm3_error( f"Undefined identifier '{var_name}' in expression", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -89,8 +90,9 @@ def _check_var_constant(cls, var_name, const_expr, expression): if const_expr and not const_var: raise_qasm3_error( f"Expected variable '{var_name}' to be constant in given expression", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -109,10 +111,11 @@ def _check_var_type(cls, var_name, reqd_type, expression): var = cls.visitor_obj._get_from_visible_scope(var_name) if not Qasm3Validator.validate_variable_type(var, reqd_type): raise_qasm3_error( - f"Invalid type '{var.base_type}' of variable '{var_name}' for " + message=f"Invalid type '{var.base_type}' of variable '{var_name}' for " f"required type {reqd_type}", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @staticmethod @@ -130,8 +133,9 @@ def _check_var_initialized(var_name, var_value, expression): if var_value is None: raise_qasm3_error( f"Uninitialized variable '{var_name}' in expression", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -183,8 +187,9 @@ def evaluate_expression( # type: ignore[return] if isinstance(expression, (ImaginaryLiteral, DurationLiteral)): raise_qasm3_error( f"Unsupported expression type {type(expression)}", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) def _check_and_return_value(value): @@ -207,8 +212,9 @@ def _process_variable(var_name: str, indices=None): return _check_and_return_value(CONSTANTS_MAP[var_name]) raise_qasm3_error( f"Constant '{var_name}' not allowed in non-float expression", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) return _process_variable(var_name) @@ -230,6 +236,7 @@ def _process_variable(var_name: str, indices=None): raise_qasm3_error( message=f"Unsupported target type '{type(target)}' for sizeof expression", err_type=ValidationError, + error_node=expression, span=expression.span, ) @@ -237,6 +244,7 @@ def _process_variable(var_name: str, indices=None): raise_qasm3_error( message=f"Invalid sizeof usage, variable '{var_name}' is not an array.", err_type=ValidationError, + error_node=expression, span=expression.span, ) @@ -251,8 +259,9 @@ def _process_variable(var_name: str, indices=None): raise_qasm3_error( f"Index {index} out of bounds for array '{var_name}' with " f"{len(dimensions)} dimensions", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) return _check_and_return_value(dimensions[index]) @@ -267,8 +276,9 @@ def _process_variable(var_name: str, indices=None): raise_qasm3_error( f"Invalid value {expression.value} with type {type(expression)} " f"for required type {reqd_type}", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) return _check_and_return_value(expression.value) @@ -279,8 +289,9 @@ def _process_variable(var_name: str, indices=None): if expression.op.name == "~" and not isinstance(operand, int): raise_qasm3_error( f"Unsupported expression type '{type(operand)}' in ~ operation", - ValidationError, - expression.span, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) op_name = "UMINUS" if expression.op.name == "-" else expression.op.name statements.extend(returned_stats) @@ -307,7 +318,10 @@ def _process_variable(var_name: str, indices=None): return _check_and_return_value(ret_value) raise_qasm3_error( - f"Unsupported expression type {type(expression)}", ValidationError, expression.span + f"Unsupported expression type {type(expression)}", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 5b6890c..685c44f 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -16,6 +16,7 @@ Definition of the base Qasm module """ +import os from abc import ABC, abstractmethod from copy import deepcopy from typing import Optional @@ -504,11 +505,12 @@ def reverse_qubit_order(self, in_place=True): # 4. return the module return qasm_module - def validate(self): + def validate(self, expand_traceback: Optional[bool] = False): """Validate the module""" if self._validated_program is True: return try: + os.environ["PYQASM_EXPAND_TRACEBACK"] = "1" if expand_traceback else "0" self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) self.accept(visitor) @@ -526,6 +528,7 @@ def unroll(self, **kwargs): unroll_barriers (bool): If True, barriers will be unrolled. Defaults to True. check_only (bool): If True, only check the program without executing it. Defaults to False. + expand_traceback (bool): If True, expand the traceback for better error messages. Raises: ValidationError: If the module fails validation during unrolling. @@ -538,6 +541,10 @@ def unroll(self, **kwargs): if not kwargs: kwargs = {} try: + os.environ["PYQASM_EXPAND_TRACEBACK"] = ( + "1" if kwargs.pop("expand_traceback", False) else "0" + ) + self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(module=self, **kwargs) self.accept(visitor) diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index 9d9acee..61a7ad6 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -122,6 +122,7 @@ def _process_classical_arg_by_value( raise_qasm3_error( f"Expecting classical argument for '{formal_arg.name.name}'. " f"Qubit register '{actual_arg_name}' found for function '{fn_name}'", + error_node=actual_arg, span=span, ) @@ -131,6 +132,7 @@ def _process_classical_arg_by_value( raise_qasm3_error( f"Undefined variable '{actual_arg_name}' used" f" for function call '{fn_name}'", + error_node=actual_arg, span=span, ) actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0] @@ -183,6 +185,7 @@ def _process_classical_arg_by_reference( array_expected_type_msg + f"Literal '{Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]}' " + "found in function call", + error_node=actual_arg, span=span, ) @@ -190,6 +193,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( array_expected_type_msg + f"Qubit register '{actual_arg_name}' found for function call", + error_node=actual_arg, span=span, ) @@ -197,6 +201,7 @@ def _process_classical_arg_by_reference( if not cls.visitor_obj._check_in_scope(actual_arg_name): raise_qasm3_error( f"Undefined variable '{actual_arg_name}' used for function call '{fn_name}'", + error_node=actual_arg, span=span, ) @@ -208,6 +213,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( array_expected_type_msg + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", + error_node=actual_arg, span=span, ) @@ -219,6 +225,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( array_expected_type_msg + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", + error_node=actual_arg, span=span, ) @@ -241,6 +248,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( f"Invalid number of dimensions {num_formal_dimensions}" f" for '{formal_arg.name.name}' in function '{fn_name}'", + error_node=formal_arg, span=span, ) @@ -249,6 +257,7 @@ def _process_classical_arg_by_reference( f"Dimension mismatch for '{formal_arg.name.name}' in function '{fn_name}'. " f"Expected {num_formal_dimensions} dimensions but" f" variable '{actual_arg_name}' has {len(actual_dimensions)}", + error_node=formal_arg, span=span, ) formal_dimensions = [] @@ -268,6 +277,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( f"Invalid dimension size {formal_dim} for '{formal_arg.name.name}'" f" in function '{fn_name}'", + error_node=formal_arg, span=span, ) if actual_dim < formal_dim: @@ -275,6 +285,7 @@ def _process_classical_arg_by_reference( f"Dimension mismatch for '{formal_arg.name.name}'" f" in function '{fn_name}'. Expected dimension {idx} with size" f" >= {formal_dim} but got {actual_dim}", + error_node=actual_arg, span=span, ) formal_dimensions.append(formal_dim) @@ -341,6 +352,7 @@ def process_quantum_arg( raise_qasm3_error( f"Invalid qubit size {formal_qubit_size} for variable '{formal_reg_name}'" f" in function '{fn_name}'", + error_node=formal_arg, span=span, ) formal_qreg_size_map[formal_reg_name] = formal_qubit_size @@ -352,6 +364,7 @@ def process_quantum_arg( raise_qasm3_error( f"Expecting qubit argument for '{formal_reg_name}'. " f"Qubit register '{actual_arg_name}' not found for function '{fn_name}'", + error_node=actual_arg, span=span, ) cls.visitor_obj._label_scope_level[cls.visitor_obj._curr_scope].add(formal_reg_name) @@ -365,6 +378,7 @@ def process_quantum_arg( f"Qubit register size mismatch for function '{fn_name}'. " f"Expected {formal_qubit_size} in variable '{formal_reg_name}' " f"but got {actual_qubits_size}", + error_node=actual_arg, span=span, ) @@ -374,6 +388,7 @@ def process_quantum_arg( raise_qasm3_error( f"Duplicate qubit argument for register '{actual_arg_name}' " f"in function call for '{fn_name}'", + error_node=actual_arg, span=span, ) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 36ba5ab..65bf045 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -112,6 +112,7 @@ def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: if not isinstance(value, IntegerLiteral): raise_qasm3_error( f"Unsupported discrete set value '{value}' in discrete set", + error_node=discrete_set, span=discrete_set.span, ) values.append(value.value) @@ -169,6 +170,7 @@ def transform_gate_qubits( raise_qasm3_error( f"Indexing '{qubit.name.name}' not supported in gate definition " f"for gate {gate_op.name}", + error_node=gate_op, span=qubit.span, ) gate_qubit_name = qubit.name @@ -257,12 +259,14 @@ def get_branch_params( raise_qasm3_error( message="Only simple comparison supported in branching condition with " "classical register", + error_node=condition, span=condition.span, ) if isinstance(condition, UnaryExpression): if condition.op != UnaryOperator["!"]: raise_qasm3_error( message="Only '!' supported in branching condition with classical register", + error_node=condition, span=condition.span, ) return BranchParams( @@ -276,6 +280,7 @@ def get_branch_params( raise_qasm3_error( message="Only {==, >=, <=, >, <} supported in branching condition " "with classical register", + error_node=condition, span=condition.span, ) @@ -301,12 +306,14 @@ def get_branch_params( if isinstance(condition.index, DiscreteSet): raise_qasm3_error( message="DiscreteSet not supported in branching condition", + error_node=condition, span=condition.span, ) if isinstance(condition.index, list): if isinstance(condition.index[0], RangeDefinition): raise_qasm3_error( message="RangeDefinition not supported in branching condition", + error_node=condition, span=condition.span, ) return BranchParams( diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index 7ae35fe..953c920 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -68,6 +68,7 @@ def validate_statement_type(blacklisted_stmts: set, statement: Any, construct: s if stmt_type != ClassicalDeclaration: raise_qasm3_error( f"Unsupported statement '{stmt_type}' in {construct} block", + error_node=statement, span=statement.span, ) @@ -75,6 +76,7 @@ def validate_statement_type(blacklisted_stmts: set, statement: Any, construct: s raise_qasm3_error( f"Unsupported statement {stmt_type} with {statement.type.__class__}" f" in {construct} block", + error_node=statement, span=statement.span, ) @@ -138,7 +140,7 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any: left, right = 0, 2**base_size - 1 if type_casted_value < left or type_casted_value > right: raise_qasm3_error( - f"Value {value} out of limits for variable {variable.name} with " + f"Value {value} out of limits for variable '{variable.name}' with " f"base size {base_size}", ) @@ -239,6 +241,7 @@ def validate_gate_call( raise_qasm3_error( f"Parameter count mismatch for gate '{operation.name.name}': " f"expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.", + error_node=operation, span=operation.span, ) @@ -248,6 +251,7 @@ def validate_gate_call( raise_qasm3_error( f"Qubit count mismatch for gate '{operation.name.name}': " f"expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.", + error_node=operation, span=operation.span, ) @@ -276,6 +280,7 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements raise_qasm3_error( f"Return type mismatch for subroutine '{subroutine_def.name.name}'." f" Expected void but got {type(return_value)}", + error_node=return_statement, span=return_statement.span, ) else: @@ -283,6 +288,7 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements raise_qasm3_error( f"Return type mismatch for subroutine '{subroutine_def.name.name}'." f" Expected {subroutine_def.return_type} but got void", + error_node=return_statement, span=return_statement.span, ) base_size = 1 diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 86cc371..65ff9b7 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -291,12 +291,14 @@ def _visit_quantum_register( if self._check_in_scope(register_name): raise_qasm3_error( f"Re-declaration of quantum register with name '{register_name}'", + error_node=register, span=register.span, ) if register_name in CONSTANTS_MAP: raise_qasm3_error( f"Can not declare quantum register with keyword name '{register_name}'", + error_node=register, span=register.span, ) @@ -339,6 +341,7 @@ def _check_if_name_in_scope(self, name: str, operation: Any) -> None: operation_name = operation.name.name if hasattr(operation.name, "name") else operation.name raise_qasm3_error( f"Variable '{name}' not in scope for {operation_type} '{operation_name}'", + error_node=operation, span=operation.span, ) @@ -400,6 +403,7 @@ def _get_op_bits( ) raise_qasm3_error( err_msg, + error_node=operation, span=operation.span, ) self._check_if_name_in_scope(reg_name, operation) @@ -461,8 +465,8 @@ def _visit_measurement( # pylint: disable=too-many-locals ) if source_name not in self._global_qreg_size_map: raise_qasm3_error( - f"Missing register declaration for {source_name} in measurement " - f"operation {statement}", + f"Missing register declaration for '{source_name}' in measurement " f"operation", + error_node=statement, span=statement.span, ) @@ -489,8 +493,9 @@ def _visit_measurement( # pylint: disable=too-many-locals ) if target_name not in self._global_creg_size_map: raise_qasm3_error( - f"Missing register declaration for {target_name} in measurement " - f"operation {statement}", + f"Missing register declaration for '{target_name}' in measurement " + f"operation", + error_node=statement, span=statement.span, ) @@ -502,6 +507,7 @@ def _visit_measurement( # pylint: disable=too-many-locals raise_qasm3_error( f"Register sizes of {source_name} and {target_name} do not match " "for measurement operation", + error_node=statement, span=statement.span, ) @@ -648,7 +654,9 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> gate_name = definition.name.name if gate_name in self._custom_gates: raise_qasm3_error( - f"Duplicate quantum gate definition for '{gate_name}'", span=definition.span + f"Duplicate quantum gate definition for '{gate_name}'", + error_node=definition, + span=definition.span, ) self._custom_gates[gate_name] = definition @@ -671,6 +679,7 @@ def _unroll_multiple_target_qubits( if len(op_qubits) <= 0 or len(op_qubits) % gate_qubit_count != 0: raise_qasm3_error( f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}", + error_node=operation, span=operation.span, ) qubit_subsets = [] @@ -792,6 +801,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals raise_qasm3_error( f"Expected {op_qubit_count} parameter{'s' if op_qubit_count > 1 else ''}" f" for gate '{operation.name.name}', but got {len(op_parameters)}", + error_node=operation, span=operation.span, ) @@ -888,6 +898,7 @@ def _visit_custom_gate_operation( if isinstance(gate_op, qasm3_ast.QuantumGate) and gate_op.name.name == gate_name: raise_qasm3_error( f"Recursive definitions not allowed for gate '{gate_name}'", + error_node=gate_op, span=gate_op.span, ) Qasm3Transformer.transform_gate_params(gate_op_copy, param_map) @@ -903,6 +914,7 @@ def _visit_custom_gate_operation( # TODO: add control flow support raise_qasm3_error( f"Unsupported statement in gate definition '{type(gate_op).__name__}'", + error_node=gate_op, span=gate_op.span, ) @@ -1038,6 +1050,7 @@ def _visit_phase_operation( if self._in_global_scope() and len(operation.qubits) != 0: raise_qasm3_error( "Qubit arguments not allowed for 'gphase' operation in global scope", + error_node=operation, span=operation.span, ) @@ -1099,6 +1112,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches except ValidationError: raise_qasm3_error( f"Power modifier argument must be an integer in gate operation {operation}", + error_node=operation, span=operation.span, ) exponent *= current_power @@ -1116,6 +1130,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches raise_qasm3_error( "Controlled modifier arguments must be compile-time constants " f"in gate operation {operation}", + error_node=operation, span=operation.span, ) if count is None: @@ -1124,6 +1139,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches raise_qasm3_error( "Controlled modifier argument must be a positive integer " f"in gate operation {operation}", + error_node=operation, span=operation.span, ) ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] @@ -1144,6 +1160,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches raise_qasm3_error( "Power modifiers with non-integer arguments are unsupported in gate " f"operation {operation}", + error_node=operation, span=operation.span, ) @@ -1188,10 +1205,14 @@ def _visit_constant_declaration( if var_name in CONSTANTS_MAP: raise_qasm3_error( - f"Can not declare variable with keyword name {var_name}", span=statement.span + f"Can not declare variable with keyword name {var_name}", + error_node=statement, + span=statement.span, ) if self._check_in_scope(var_name): - raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) + raise_qasm3_error( + f"Re-declaration of variable {var_name}", error_node=statement, span=statement.span + ) init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( statement.init_expression, const_expr=True ) @@ -1210,6 +1231,7 @@ def _visit_constant_declaration( if not isinstance(base_size, int) or base_size <= 0: raise_qasm3_error( f"Invalid base size {base_size} for variable '{var_name}'", + error_node=statement, span=statement.span, ) @@ -1241,7 +1263,9 @@ def _visit_classical_declaration( var_name = statement.identifier.name if var_name in CONSTANTS_MAP: raise_qasm3_error( - f"Can not declare variable with keyword name {var_name}", span=statement.span + f"Can not declare variable with keyword name {var_name}", + error_node=statement, + span=statement.span, ) if self._check_in_scope(var_name): if self._in_block_scope() and var_name not in self._get_curr_scope(): @@ -1251,7 +1275,11 @@ def _visit_classical_declaration( # { int a = 20;} // is valid pass else: - raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) + raise_qasm3_error( + f"Re-declaration of variable {var_name}", + error_node=statement, + span=statement.span, + ) init_value = None base_type = statement.type @@ -1281,12 +1309,15 @@ def _visit_classical_declaration( # bit type arrays are not allowed if isinstance(base_type, qasm3_ast.BitType): raise_qasm3_error( - f"Can not declare array {var_name} with type 'bit'", span=statement.span + f"Can not declare array {var_name} with type 'bit'", + error_node=statement, + span=statement.span, ) if len(dimensions) > MAX_ARRAY_DIMENSIONS: raise_qasm3_error( f"Invalid dimensions {len(dimensions)} for array declaration for '{var_name}'. " f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}", + error_node=statement, span=statement.span, ) @@ -1295,6 +1326,7 @@ def _visit_classical_declaration( if not isinstance(dim_value, int) or dim_value <= 0: raise_qasm3_error( f"Invalid dimension size {dim_value} in array declaration for '{var_name}'", + error_node=statement, span=statement.span, ) final_dimensions.append(dim_value) @@ -1380,10 +1412,16 @@ def _visit_classical_assignment( lvar = self._get_from_visible_scope(lvar_name) if lvar is None: # we check for none here, so type errors are irrelevant afterwards - raise_qasm3_error(f"Undefined variable {lvar_name} in assignment", span=statement.span) + raise_qasm3_error( + f"Undefined variable {lvar_name} in assignment", + error_node=statement, + span=statement.span, + ) if lvar.is_constant: # type: ignore[union-attr] raise_qasm3_error( - f"Assignment to constant variable {lvar_name} not allowed", span=statement.span + f"Assignment to constant variable {lvar_name} not allowed", + error_node=statement, + span=statement.span, ) binary_op: str | None | qasm3_ast.BinaryOperator = None if statement.op != qasm3_ast.AssignmentOperator["="]: @@ -1424,6 +1462,7 @@ def _visit_classical_assignment( if lvar.readonly: # type: ignore[union-attr] raise_qasm3_error( f"Assignment to readonly variable '{lvar_name}' not allowed in function call", + error_node=statement, span=statement.span, ) @@ -1496,7 +1535,7 @@ def _visit_branching_statement( condition = statement.condition if not statement.if_block: - raise_qasm3_error("Missing if block", span=statement.span) + raise_qasm3_error("Missing if block", error_node=statement, span=statement.span) if Qasm3ExprEvaluator.classical_register_in_expr(condition): # leave this condition as is, and start unrolling the block @@ -1508,7 +1547,8 @@ def _visit_branching_statement( if reg_name not in self._global_creg_size_map: raise_qasm3_error( - f"Missing register declaration for {reg_name} in {condition}", + f"Missing register declaration for '{reg_name}' in branching statement", + error_node=condition, span=statement.span, ) @@ -1676,16 +1716,21 @@ def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition if fn_name in CONSTANTS_MAP: raise_qasm3_error( - f"Subroutine name '{fn_name}' is a reserved keyword", span=statement.span + f"Subroutine name '{fn_name}' is a reserved keyword", + error_node=statement, + span=statement.span, ) if fn_name in self._subroutine_defns: - raise_qasm3_error(f"Redefinition of subroutine '{fn_name}'", span=statement.span) + raise_qasm3_error( + f"Redefinition of subroutine '{fn_name}'", error_node=statement, span=statement.span + ) if self._check_in_scope(fn_name): raise_qasm3_error( f"Can not declare subroutine with name '{fn_name}' as " "it is already declared as a variable", + error_node=statement, span=statement.span, ) @@ -1707,7 +1752,11 @@ def _visit_function_call( """ fn_name = statement.name.name if fn_name not in self._subroutine_defns: - raise_qasm3_error(f"Undefined subroutine '{fn_name}' was called", span=statement.span) + raise_qasm3_error( + f"Undefined subroutine '{fn_name}' was called", + error_node=statement, + span=statement.span, + ) subroutine_def = self._subroutine_defns[fn_name] @@ -1715,6 +1764,7 @@ def _visit_function_call( raise_qasm3_error( f"Parameter count mismatch for subroutine '{fn_name}'. Expected " f"{len(subroutine_def.arguments)} but got {len(statement.arguments)} in call", + error_node=statement, span=statement.span, ) @@ -1822,7 +1872,11 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No # Alias should not be redeclared earlier as a variable or a constant if self._check_in_scope(alias_reg_name): - raise_qasm3_error(f"Re-declaration of variable '{alias_reg_name}'", span=statement.span) + raise_qasm3_error( + f"Re-declaration of variable '{alias_reg_name}'", + error_node=statement, + span=statement.span, + ) self._label_scope_level[self._curr_scope].add(alias_reg_name) if isinstance(value, qasm3_ast.Identifier): @@ -1832,11 +1886,15 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No ): aliased_reg_name = value.collection.name else: - raise_qasm3_error(f"Unsupported aliasing {statement}", span=statement.span) + raise_qasm3_error( + f"Unsupported aliasing {statement}", error_node=statement, span=statement.span + ) if aliased_reg_name not in self._global_qreg_size_map: raise_qasm3_error( - f"Qubit register {aliased_reg_name} not found for aliasing", span=statement.span + f"Qubit register {aliased_reg_name} not found for aliasing", + error_node=statement, + span=statement.span, ) aliased_reg_size = self._global_qreg_size_map[aliased_reg_name] if isinstance(value, qasm3_ast.Identifier): # "let alias = q;" @@ -1857,6 +1915,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No "An index set can be specified by a single integer (signed or unsigned), " "a comma-separated list of integers contained in braces {a,b,c,…}, " "or a range", + error_node=statement, span=statement.span, ) elif isinstance(value.index[0], qasm3_ast.IntegerLiteral): # "let alias = q[0];" @@ -1909,13 +1968,19 @@ def _visit_switch_statement( # type: ignore[return] self._get_from_visible_scope(switch_target_name), qasm3_ast.IntType ): raise_qasm3_error( - f"Switch target {switch_target_name} must be of type int", span=statement.span + f"Switch target {switch_target_name} must be of type int", + error_node=statement, + span=statement.span, ) switch_target_val = Qasm3ExprEvaluator.evaluate_expression(switch_target)[0] if len(statement.cases) == 0: - raise_qasm3_error("Switch statement must have at least one case", span=statement.span) + raise_qasm3_error( + "Switch statement must have at least one case", + error_node=statement, + span=statement.span, + ) # 2. handle the cases of the switch stmt # each element in the list of the values @@ -1951,7 +2016,9 @@ def _evaluate_case(statements): if case_val in seen_values: raise_qasm3_error( - f"Duplicate case value {case_val} in switch statement", span=case_expr.span + f"Duplicate case value {case_val} in switch statement", + error_node=case_expr, + span=case_expr.span, ) seen_values.add(case_val) @@ -1978,7 +2045,9 @@ def _visit_include(self, include: qasm3_ast.Include) -> list[qasm3_ast.Statement """ filename = include.filename if filename in self._included_files: - raise_qasm3_error(f"File '{filename}' already included", span=include.span) + raise_qasm3_error( + f"File '{filename}' already included", error_node=include, span=include.span + ) self._included_files.add(filename) if self._check_only: return [] @@ -2028,7 +2097,9 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat result.extend(visitor_function(statement)) # type: ignore[operator] else: raise_qasm3_error( - f"Unsupported statement of type {type(statement)}", span=statement.span + f"Unsupported statement of type {type(statement)}", + error_node=statement, + span=statement.span, ) return result diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index b2c7d1c..959c87b 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -231,7 +231,7 @@ int[32] x = 1<<64; """, - f"Value {2**64} out of limits for variable x with base size 32", + f"Value {2**64} out of limits for variable 'x' with base size 32", ), "float32_out_of_range": ( """ diff --git a/tests/qasm3/test_measurement.py b/tests/qasm3/test_measurement.py index acb3142..0830cec 100644 --- a/tests/qasm3/test_measurement.py +++ b/tests/qasm3/test_measurement.py @@ -202,7 +202,7 @@ def run_test(qasm3_code, error_message): bit[2] c1; c1[0] = measure q2[0]; // undeclared register """, - r"Missing register declaration for q2 .*", + r"Missing register declaration for 'q2' .*", ) # Test for undeclared register c2 @@ -213,7 +213,7 @@ def run_test(qasm3_code, error_message): bit[2] c1; measure q1 -> c2; // undeclared register """, - r"Missing register declaration for c2 .*", + r"Missing register declaration for 'c2' .*", ) # Test for size mismatch between q1 and c2 From 17ae3510cd5475b0ba42b3d7050f1eb439eb12b5 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 15 Apr 2025 13:14:53 +0530 Subject: [PATCH 04/16] add verbosity to msgs --- src/pyqasm/exceptions.py | 8 +++- src/pyqasm/expressions.py | 2 +- src/pyqasm/subroutines.py | 62 ++++++++++++++++++---------- src/pyqasm/transformer.py | 15 ++++--- src/pyqasm/validator.py | 42 +++++++++++++++---- src/pyqasm/visitor.py | 27 +++++++----- tests/qasm3/resources/subroutines.py | 2 +- tests/qasm3/resources/variables.py | 6 +-- 8 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 994c810..36498f6 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -84,7 +84,13 @@ def raise_qasm3_error( if error_node: try: - error_parts.append("\n >>>>>> " + dumps(error_node, indent=" ")) + if isinstance(error_node, QASMNode): + error_parts.append("\n >>>>>> " + dumps(error_node, indent=" ") + "\n") + elif isinstance(error_node, list): + error_parts.append( + "\n >>>>>> " + + " , ".join(dumps(node, indent=" ") for node in error_node + "\n") + ) except Exception as _: # pylint: disable = broad-exception-caught error_parts.append("\n >>>>>> " + str(error_node)) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 87e2a6b..f89ad62 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -186,7 +186,7 @@ def evaluate_expression( # type: ignore[return] if isinstance(expression, (ImaginaryLiteral, DurationLiteral)): raise_qasm3_error( - f"Unsupported expression type {type(expression)}", + f"Unsupported expression type '{type(expression)}'", err_type=ValidationError, error_node=expression, span=expression.span, diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index 61a7ad6..e30c926 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -26,6 +26,7 @@ IntType, QubitDeclaration, ) +from openqasm3.printer import dumps from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import Variable @@ -117,12 +118,15 @@ def _process_classical_arg_by_value( # 1. variable mapping is equivalent to declaring the variable # with the formal argument name and doing classical assignment # in the scope of the function + + fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + if actual_arg_name: # actual arg is a variable not literal if actual_arg_name in cls.visitor_obj._global_qreg_size_map: raise_qasm3_error( f"Expecting classical argument for '{formal_arg.name.name}'. " f"Qubit register '{actual_arg_name}' found for function '{fn_name}'", - error_node=actual_arg, + error_node=fn_defn.arguments, span=span, ) @@ -180,6 +184,8 @@ def _process_classical_arg_by_reference( f" in function '{fn_name}'. " ) + fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + if actual_arg_name is None: raise_qasm3_error( array_expected_type_msg @@ -213,7 +219,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( array_expected_type_msg + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", - error_node=actual_arg, + error_node=fn_defn.arguments, span=span, ) @@ -225,7 +231,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( array_expected_type_msg + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", - error_node=actual_arg, + error_node=fn_defn.arguments, span=span, ) @@ -248,7 +254,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( f"Invalid number of dimensions {num_formal_dimensions}" f" for '{formal_arg.name.name}' in function '{fn_name}'", - error_node=formal_arg, + error_node=fn_defn.arguments, span=span, ) @@ -257,7 +263,7 @@ def _process_classical_arg_by_reference( f"Dimension mismatch for '{formal_arg.name.name}' in function '{fn_name}'. " f"Expected {num_formal_dimensions} dimensions but" f" variable '{actual_arg_name}' has {len(actual_dimensions)}", - error_node=formal_arg, + error_node=fn_defn.arguments, span=span, ) formal_dimensions = [] @@ -277,7 +283,7 @@ def _process_classical_arg_by_reference( raise_qasm3_error( f"Invalid dimension size {formal_dim} for '{formal_arg.name.name}'" f" in function '{fn_name}'", - error_node=formal_arg, + error_node=fn_defn.arguments, span=span, ) if actual_dim < formal_dim: @@ -285,7 +291,7 @@ def _process_classical_arg_by_reference( f"Dimension mismatch for '{formal_arg.name.name}'" f" in function '{fn_name}'. Expected dimension {idx} with size" f" >= {formal_dim} but got {actual_dim}", - error_node=actual_arg, + error_node=fn_defn.arguments, span=span, ) formal_dimensions.append(formal_dim) @@ -311,7 +317,7 @@ def _process_classical_arg_by_reference( ) @classmethod # pylint: disable-next=too-many-arguments - def process_quantum_arg( + def process_quantum_arg( # pylint: disable=too-many-locals cls, formal_arg, actual_arg, @@ -319,7 +325,7 @@ def process_quantum_arg( duplicate_qubit_map, qubit_transform_map, fn_name, - span, + fn_call, ): """ Process a quantum argument in the QASM3 visitor. @@ -331,7 +337,7 @@ def process_quantum_arg( duplicate_qubit_map (dict): The map of duplicate qubit registers. qubit_transform_map (dict): The map of qubit register transformations. fn_name (str): The name of the function. - span (Span): The span of the function call. + fn_call (qasm3_ast.FunctionCall) : The function call node in the AST. Returns: list: The list of actual qubit ids. @@ -348,12 +354,15 @@ def process_quantum_arg( )[0] if formal_qubit_size is None: formal_qubit_size = 1 + + fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + if formal_qubit_size <= 0: raise_qasm3_error( - f"Invalid qubit size {formal_qubit_size} for variable '{formal_reg_name}'" + f"Invalid qubit size '{formal_qubit_size}' for variable '{formal_reg_name}'" f" in function '{fn_name}'", - error_node=formal_arg, - span=span, + error_node=fn_defn.arguments, + span=formal_arg.span, ) formal_qreg_size_map[formal_reg_name] = formal_qubit_size @@ -361,11 +370,20 @@ def process_quantum_arg( # note that we ONLY check in global scope as # we always map the qubit arguments to the global scope if actual_arg_name not in cls.visitor_obj._global_qreg_size_map: + # Check if the actual argument is a qubit register + is_literal = actual_arg_name is None + arg_desc = ( + f"Literal '{Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]}' " + if is_literal + else f"Qubit register '{actual_arg_name}' not " + ) + formal_args_desc = " , ".join(dumps(arg, indent=" ") for arg in fn_defn.arguments) raise_qasm3_error( f"Expecting qubit argument for '{formal_reg_name}'. " - f"Qubit register '{actual_arg_name}' not found for function '{fn_name}'", - error_node=actual_arg, - span=span, + f"{arg_desc}found for function '{fn_name}'\n" + f"Usage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) cls.visitor_obj._label_scope_level[cls.visitor_obj._curr_scope].add(formal_reg_name) @@ -374,12 +392,14 @@ def process_quantum_arg( ) if formal_qubit_size != actual_qubits_size: + formal_args_desc = " , ".join(dumps(arg, indent=" ") for arg in fn_defn.arguments) raise_qasm3_error( f"Qubit register size mismatch for function '{fn_name}'. " f"Expected {formal_qubit_size} in variable '{formal_reg_name}' " - f"but got {actual_qubits_size}", - error_node=actual_arg, - span=span, + f"but got {actual_qubits_size}\n", + f"Usage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) if not Qasm3Validator.validate_unique_qubits( @@ -388,8 +408,8 @@ def process_quantum_arg( raise_qasm3_error( f"Duplicate qubit argument for register '{actual_arg_name}' " f"in function call for '{fn_name}'", - error_node=actual_arg, - span=span, + error_node=fn_call, + span=fn_call.span, ) for idx, qid in enumerate(actual_qids): diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 65bf045..6c159db 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -111,7 +111,8 @@ def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: for value in discrete_set.values: if not isinstance(value, IntegerLiteral): raise_qasm3_error( - f"Unsupported discrete set value '{value}' in discrete set", + f"Unsupported value '{Qasm3ExprEvaluator.evaluate_expression(value)[0]}' " + "in discrete set", error_node=discrete_set, span=discrete_set.span, ) @@ -145,8 +146,12 @@ def get_qubits_from_range_definition( if range_def.step is None else Qasm3ExprEvaluator.evaluate_expression(range_def.step)[0] ) - Qasm3Validator.validate_register_index(start_qid, qreg_size, qubit=is_qubit_reg) - Qasm3Validator.validate_register_index(end_qid - 1, qreg_size, qubit=is_qubit_reg) + Qasm3Validator.validate_register_index( + start_qid, qreg_size, qubit=is_qubit_reg, op_node=range_def + ) + Qasm3Validator.validate_register_index( + end_qid - 1, qreg_size, qubit=is_qubit_reg, op_node=range_def + ) return list(range(start_qid, end_qid, step)) @staticmethod @@ -390,13 +395,13 @@ def get_target_qubits( target_qids = Qasm3Transformer.extract_values_from_discrete_set(target.index) for qid in target_qids: Qasm3Validator.validate_register_index( - qid, qreg_size_map[target_name], qubit=True + qid, qreg_size_map[target_name], qubit=True, op_node=target ) target_qubits_size = len(target_qids) elif isinstance(target.index[0], (IntegerLiteral, Identifier)): # "(q[0]); OR (q[i]);" target_qids = [Qasm3ExprEvaluator.evaluate_expression(target.index[0])[0]] Qasm3Validator.validate_register_index( - target_qids[0], qreg_size_map[target_name], qubit=True + target_qids[0], qreg_size_map[target_name], qubit=True, op_node=target ) target_qubits_size = 1 elif isinstance(target.index[0], RangeDefinition): # "(q[0:1:2]);" diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index 953c920..e4f441d 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -19,9 +19,19 @@ from typing import Any, Optional import numpy as np -from openqasm3.ast import ArrayType, ClassicalDeclaration, FloatType +from openqasm3.ast import ( + ArrayType, + ClassicalDeclaration, + FloatType, +) from openqasm3.ast import IntType as Qasm3IntType -from openqasm3.ast import QuantumGate, QuantumGateDefinition, ReturnStatement, SubroutineDefinition +from openqasm3.ast import ( + QASMNode, + QuantumGate, + QuantumGateDefinition, + ReturnStatement, + SubroutineDefinition, +) from pyqasm.elements import Variable from pyqasm.exceptions import ValidationError, raise_qasm3_error @@ -32,7 +42,9 @@ class Qasm3Validator: """Class with validation functions for QASM visitor""" @staticmethod - def validate_register_index(index: Optional[int], size: int, qubit: bool = False) -> None: + def validate_register_index( + index: Optional[int], size: int, qubit: bool = False, op_node: Optional[Any] = None + ) -> None: """Validate the index for a register. Args: @@ -44,11 +56,13 @@ def validate_register_index(index: Optional[int], size: int, qubit: bool = False ValidationError: If the index is out of range. """ if index is None or 0 <= index < size: - return None + return - raise ValidationError( - f"Index {index} out of range for register of size {size} in " - f"{'qubit' if qubit else 'clbit'}" + raise_qasm3_error( + message=f"Index {index} out of range for register of size {size} in " + f"{'qubit' if qubit else 'clbit'}", + error_node=op_node, + span=op_node.span if op_node else None, ) @staticmethod @@ -98,7 +112,9 @@ def validate_variable_type(variable: Optional[Variable], reqd_type: Any) -> bool return isinstance(variable.base_type, reqd_type) @staticmethod - def validate_variable_assignment_value(variable: Variable, value) -> Any: + def validate_variable_assignment_value( + variable: Variable, value, op_node: Optional[QASMNode] = None + ) -> Any: """Validate the assignment of a value to a variable. Args: @@ -142,6 +158,8 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any: raise_qasm3_error( f"Value {value} out of limits for variable '{variable.name}' with " f"base size {base_size}", + error_node=op_node, + span=op_node.span if op_node else None, ) elif type_to_match == float: @@ -156,12 +174,17 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any: raise_qasm3_error( f"Value {value} out of limits for variable '{variable.name}' with " f"base size {base_size}", + error_node=op_node, + span=op_node.span if op_node else None, ) elif type_to_match == bool: pass else: raise_qasm3_error( - f"Invalid type {type_to_match} for variable '{variable.name}'", TypeError + f"Invalid type {type_to_match} for variable '{variable.name}'", + TypeError, + error_node=op_node, + span=op_node.span if op_node else None, ) return type_casted_value @@ -304,6 +327,7 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements None, ), return_value, + op_node=return_statement, ) @staticmethod diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 65ff9b7..4771260 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -418,7 +418,7 @@ def _get_op_bits( else: bit_id = Qasm3ExprEvaluator.evaluate_expression(bit.indices[0][0])[0] Qasm3Validator.validate_register_index( - bit_id, reg_size_map[reg_name], qubit=qubits + bit_id, reg_size_map[reg_name], qubit=qubits, op_node=operation ) bit_ids = [bit_id] else: @@ -1211,7 +1211,9 @@ def _visit_constant_declaration( ) if self._check_in_scope(var_name): raise_qasm3_error( - f"Re-declaration of variable {var_name}", error_node=statement, span=statement.span + f"Re-declaration of variable '{var_name}'", + error_node=statement, + span=statement.span, ) init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( statement.init_expression, const_expr=True @@ -1238,7 +1240,9 @@ def _visit_constant_declaration( variable = Variable(var_name, base_type, base_size, [], init_value, is_constant=True) # cast + validation - variable.value = Qasm3Validator.validate_variable_assignment_value(variable, init_value) + variable.value = Qasm3Validator.validate_variable_assignment_value( + variable, init_value, op_node=statement + ) self._add_var_in_scope(variable) @@ -1276,7 +1280,7 @@ def _visit_classical_declaration( pass else: raise_qasm3_error( - f"Re-declaration of variable {var_name}", + f"Re-declaration of variable '{var_name}'", error_node=statement, span=statement.span, ) @@ -1366,7 +1370,7 @@ def _visit_classical_declaration( Qasm3Validator.validate_array_assignment_values(variable, variable.dims, init_value) else: variable.value = Qasm3Validator.validate_variable_assignment_value( - variable, init_value + variable, init_value, op_node=statement ) self._add_var_in_scope(variable) @@ -1446,7 +1450,7 @@ def _visit_classical_assignment( if not isinstance(rvalue_raw, np.ndarray): # rhs is a scalar rvalue_eval = Qasm3Validator.validate_variable_assignment_value( - lvar, rvalue_raw # type: ignore[arg-type] + lvar, rvalue_raw, op_node=statement # type: ignore[arg-type] ) else: # rhs is a list rvalue_dimensions = list(rvalue_raw.shape) @@ -1560,7 +1564,7 @@ def _visit_branching_statement( if reg_idx is not None: # single bit branch Qasm3Validator.validate_register_index( - reg_idx, self._global_creg_size_map[reg_name], qubit=False + reg_idx, self._global_creg_size_map[reg_name], qubit=False, op_node=condition ) new_if_block = qasm3_ast.BranchingStatement( @@ -1789,7 +1793,7 @@ def _visit_function_call( duplicate_qubit_detect_map, qubit_transform_map, fn_name, - statement.span, + statement, ) ) @@ -1906,7 +1910,10 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No qids = Qasm3Transformer.extract_values_from_discrete_set(value.index) for i, qid in enumerate(qids): Qasm3Validator.validate_register_index( - qid, self._global_qreg_size_map[aliased_reg_name], qubit=True + qid, + self._global_qreg_size_map[aliased_reg_name], + qubit=True, + op_node=statement, ) self._alias_qubit_labels[(alias_reg_name, i)] = (aliased_reg_name, qid) alias_reg_size = len(qids) @@ -1921,7 +1928,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No elif isinstance(value.index[0], qasm3_ast.IntegerLiteral): # "let alias = q[0];" qid = value.index[0].value Qasm3Validator.validate_register_index( - qid, self._global_qreg_size_map[aliased_reg_name], qubit=True + qid, self._global_qreg_size_map[aliased_reg_name], qubit=True, op_node=statement ) self._alias_qubit_labels[(alias_reg_name, 0)] = ( aliased_reg_name, diff --git a/tests/qasm3/resources/subroutines.py b/tests/qasm3/resources/subroutines.py index 9ce8b80..26853b3 100644 --- a/tests/qasm3/resources/subroutines.py +++ b/tests/qasm3/resources/subroutines.py @@ -55,7 +55,7 @@ def my_function(qubit q) { qubit q; my_function(q); """, - "Re-declaration of variable q", + "Re-declaration of variable 'q'", ), "incorrect_param_count_1": ( """ diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index 959c87b..b77cc54 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -42,7 +42,7 @@ float y = 3.4; uint x; """, - "Re-declaration of variable x", + "Re-declaration of variable 'x'", ), "variable_redeclaration_with_qubits_1": ( """ @@ -60,7 +60,7 @@ qubit x; int x; """, - "Re-declaration of variable x", + "Re-declaration of variable 'x'", ), "const_variable_redeclaration": ( """ @@ -69,7 +69,7 @@ const int x = 3; const float x = 3.4; """, - "Re-declaration of variable x", + "Re-declaration of variable 'x'", ), "invalid_int_size": ( """ From 21e7b1f1841faa6e48e1f45bee4cb43b85e83c09 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 15 Apr 2025 13:25:30 +0530 Subject: [PATCH 05/16] fix tests --- src/pyqasm/subroutines.py | 2 +- tests/qasm3/declarations/test_quantum.py | 2 +- tests/qasm3/resources/subroutines.py | 2 +- tests/qasm3/resources/variables.py | 2 +- tests/qasm3/test_alias.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index e30c926..214c5bb 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -396,7 +396,7 @@ def process_quantum_arg( # pylint: disable=too-many-locals raise_qasm3_error( f"Qubit register size mismatch for function '{fn_name}'. " f"Expected {formal_qubit_size} in variable '{formal_reg_name}' " - f"but got {actual_qubits_size}\n", + f"but got {actual_qubits_size}\n" f"Usage: {fn_name} ( {formal_args_desc} )\n", error_node=fn_call, span=fn_call.span, diff --git a/tests/qasm3/declarations/test_quantum.py b/tests/qasm3/declarations/test_quantum.py index 7fa9508..2e053b2 100644 --- a/tests/qasm3/declarations/test_quantum.py +++ b/tests/qasm3/declarations/test_quantum.py @@ -152,7 +152,7 @@ def test_invalid_qubit_name(): def test_clbit_redeclaration_error(): """Test redeclaration of clbit""" - with pytest.raises(ValidationError, match=r"Re-declaration of variable c1"): + with pytest.raises(ValidationError, match=r"Re-declaration of variable 'c1'"): qasm3_string = """ OPENQASM 3.0; include "stdgates.inc"; diff --git a/tests/qasm3/resources/subroutines.py b/tests/qasm3/resources/subroutines.py index 26853b3..7d8039b 100644 --- a/tests/qasm3/resources/subroutines.py +++ b/tests/qasm3/resources/subroutines.py @@ -184,7 +184,7 @@ def my_function(qubit[-3] q) { qubit[4] q; my_function(q); """, - "Invalid qubit size -3 for variable 'q' in function 'my_function'", + "Invalid qubit size '-3' for variable 'q' in function 'my_function'", ), "test_type_mismatch_for_function": ( """ diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index b77cc54..b749694 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -130,7 +130,7 @@ int x = 1 + 3im; """, - "Unsupported expression type ", + "Unsupported expression type ''", ), "invalid_array_dimensions": ( """ diff --git a/tests/qasm3/test_alias.py b/tests/qasm3/test_alias.py index e3d7354..9774a2a 100644 --- a/tests/qasm3/test_alias.py +++ b/tests/qasm3/test_alias.py @@ -145,7 +145,7 @@ def test_alias_invalid_discrete_indexing(): """Test converting OpenQASM 3 program with invalid alias discrete indexing.""" with pytest.raises( ValidationError, - match=r"Unsupported discrete set value .*", + match=r"Unsupported value .*", ): qasm3_alias_program = """ OPENQASM 3.0; From 552e4284c1e496537aa46d1db130202f4166f04c Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 15 Apr 2025 15:21:50 +0530 Subject: [PATCH 06/16] adding verbosity and clarity --- src/pyqasm/maps/expressions.py | 32 +++++++++------------ src/pyqasm/subroutines.py | 6 ++-- src/pyqasm/validator.py | 18 +++++++----- src/pyqasm/visitor.py | 7 +++-- tests/qasm3/resources/gates.py | 4 +-- tests/qasm3/resources/variables.py | 2 +- tests/qasm3/subroutines/test_subroutines.py | 2 +- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/pyqasm/maps/expressions.py b/src/pyqasm/maps/expressions.py index 0f19b07..62b69d8 100644 --- a/src/pyqasm/maps/expressions.py +++ b/src/pyqasm/maps/expressions.py @@ -1,23 +1,19 @@ -# Copyright 2025 qBraid +# Copyright (C) 2025 qBraid # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# This file is part of PyQASM # -# http://www.apache.org/licenses/LICENSE-2.0 +# PyQASM is free software released under the GNU General Public License v3 +# or later. You can redistribute and/or modify it under the terms of the GPL v3. +# See the LICENSE file in the project root or . # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. """ Module mapping supported QASM expressions to lower level gate operations. """ -from typing import Callable +from typing import Callable, Union import numpy as np from openqasm3.ast import AngleType, BitType, BoolType, ComplexType, FloatType, IntType, UintType @@ -25,10 +21,10 @@ from pyqasm.exceptions import ValidationError # Define the type for the operator functions -OperatorFunction = ( - Callable[[int | float | bool], int | float | bool] - | Callable[[int | float | bool, int | float | bool], int | float | bool] -) +OperatorFunction = Union[ + Callable[[Union[int, float, bool]], Union[int, float, bool]], + Callable[[Union[int, float, bool], Union[int, float, bool]], Union[int, float, bool]], +] OPERATOR_MAP: dict[str, OperatorFunction] = { @@ -56,18 +52,18 @@ } -def qasm3_expression_op_map(op_name: str, *args) -> float | int | bool: +def qasm3_expression_op_map(op_name: str, *args) -> Union[float, int, bool]: """ Return the result of applying the given operator to the given operands. Args: op_name (str): The operator name. - *args: The operands of type int | float | bool + *args: The operands of type Union[int, float, bool] 1. For unary operators, a single operand (e.g., ~3) 2. For binary operators, two operands (e.g., 3 + 2) Returns: - (float | int | bool): The result of applying the operator to the operands. + (Union[float, int, bool]): The result of applying the operator to the operands. """ try: operator = OPERATOR_MAP[op_name] diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index 214c5bb..d433838 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -381,7 +381,7 @@ def process_quantum_arg( # pylint: disable=too-many-locals raise_qasm3_error( f"Expecting qubit argument for '{formal_reg_name}'. " f"{arg_desc}found for function '{fn_name}'\n" - f"Usage: {fn_name} ( {formal_args_desc} )\n", + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", error_node=fn_call, span=fn_call.span, ) @@ -395,9 +395,9 @@ def process_quantum_arg( # pylint: disable=too-many-locals formal_args_desc = " , ".join(dumps(arg, indent=" ") for arg in fn_defn.arguments) raise_qasm3_error( f"Qubit register size mismatch for function '{fn_name}'. " - f"Expected {formal_qubit_size} in variable '{formal_reg_name}' " + f"Expected {formal_qubit_size} qubits in variable '{formal_reg_name}' " f"but got {actual_qubits_size}\n" - f"Usage: {fn_name} ( {formal_args_desc} )\n", + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", error_node=fn_call, span=fn_call.span, ) diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index e4f441d..a990eb3 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -134,9 +134,13 @@ def validate_variable_assignment_value( try: type_to_match = VARIABLE_TYPE_MAP[qasm_type] except KeyError as err: - raise ValidationError( - f"Invalid type {qasm_type} for variable '{variable.name}'" - ) from err + raise_qasm3_error( + f"Invalid type '{qasm_type}' for variable '{variable.name}'", + err_type=ValidationError, + raised_from=err, + error_node=op_node, + span=op_node.span if op_node else None, + ) # For each type we will have a "castable" type set and its corresponding cast operation type_casted_value = qasm_variable_type_cast(qasm_type, variable.name, base_size, value) @@ -262,8 +266,8 @@ def validate_gate_call( if op_num_args != gate_def_num_args: s = "" if gate_def_num_args == 1 else "s" raise_qasm3_error( - f"Parameter count mismatch for gate '{operation.name.name}': " - f"expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.", + f"Parameter count mismatch for gate '{operation.name.name}'. " + f"Expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.", error_node=operation, span=operation.span, ) @@ -272,8 +276,8 @@ def validate_gate_call( if qubits_in_op != gate_def_num_qubits: s = "" if gate_def_num_qubits == 1 else "s" raise_qasm3_error( - f"Qubit count mismatch for gate '{operation.name.name}': " - f"expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.", + f"Qubit count mismatch for gate '{operation.name.name}'. " + f"Expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.", error_node=operation, span=operation.span, ) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 4771260..bee2f75 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1650,6 +1650,7 @@ def ravel(bit_ind): def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.Statement]: # Compute loop variable values + irange = [] if isinstance(statement.set_declaration, qasm3_ast.RangeDefinition): init_exp = statement.set_declaration.start startval = Qasm3ExprEvaluator.evaluate_expression(init_exp)[0] @@ -1668,8 +1669,10 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.St for exp in statement.set_declaration.values ] else: - raise ValidationError( - f"Unexpected type {type(statement.set_declaration)} of set_declaration in loop." + raise_qasm3_error( + f"Unexpected type {type(statement.set_declaration)} of set_declaration in loop.", + error_node=statement, + span=statement.span, ) i: Optional[Variable] # will store iteration Variable to update to loop scope diff --git a/tests/qasm3/resources/gates.py b/tests/qasm3/resources/gates.py index a538922..c177f94 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -353,7 +353,7 @@ def test_fixture(): qubit[2] q1; custom_gate(0.5) q1; // parameter count mismatch """, - "Parameter count mismatch for gate 'custom_gate': expected 2 arguments, but got 1 instead.", + "Parameter count mismatch for gate 'custom_gate'. Expected 2 arguments, but got 1 instead.", ), "parameter_mismatch_2": ( """ @@ -382,7 +382,7 @@ def test_fixture(): qubit[3] q1; custom_gate(0.5, 0.5) q1; // qubit count mismatch """, - "Qubit count mismatch for gate 'custom_gate': expected 2 qubits, but got 3 instead.", + "Qubit count mismatch for gate 'custom_gate'. Expected 2 qubits, but got 3 instead.", ), "indexing_not_supported": ( """ diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index b749694..03b51d4 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -121,7 +121,7 @@ angle x = 3.4; """, - "Invalid type for variable 'x'", + "Invalid type '' for variable 'x'", ), "imaginary_variable": ( """ diff --git a/tests/qasm3/subroutines/test_subroutines.py b/tests/qasm3/subroutines/test_subroutines.py index 3862ea7..fe5399e 100644 --- a/tests/qasm3/subroutines/test_subroutines.py +++ b/tests/qasm3/subroutines/test_subroutines.py @@ -365,7 +365,7 @@ def my_function(qubit[3] q) { with pytest.raises( ValidationError, match="Qubit register size mismatch for function 'my_function'. " - "Expected 3 in variable 'q' but got 2", + "Expected 3 qubits in variable 'q' but got 2", ): loads(qasm_str).validate() From f0c083b35f4fca9082a0e83ff1f5e493f18a3db9 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 15 Apr 2025 15:24:23 +0530 Subject: [PATCH 07/16] header fix --- src/pyqasm/maps/expressions.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pyqasm/maps/expressions.py b/src/pyqasm/maps/expressions.py index 62b69d8..4a1633d 100644 --- a/src/pyqasm/maps/expressions.py +++ b/src/pyqasm/maps/expressions.py @@ -1,12 +1,16 @@ -# Copyright (C) 2025 qBraid +# Copyright 2025 qBraid # -# This file is part of PyQASM +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# PyQASM is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . +# http://www.apache.org/licenses/LICENSE-2.0 # -# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Module mapping supported QASM expressions to lower level gate operations. From 23911c588c587a021ff2f18a59b233331852b579 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 16 Apr 2025 12:53:31 +0530 Subject: [PATCH 08/16] review changes --- src/pyqasm/exceptions.py | 2 +- src/pyqasm/maps/expressions.py | 16 ++++++++-------- src/pyqasm/modules/base.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 36498f6..8fb6ce8 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -97,7 +97,7 @@ def raise_qasm3_error( if error_parts: logger.error("\n".join(error_parts)) - if os.environ.get("PYQASM_EXPAND_TRACEBACK") == "0": + if os.environ.get("PYQASM_EXPAND_TRACEBACK") == "false": sys.tracebacklimit = 0 # Disable traceback for cleaner output # Extract the latest message from the traceback if raised_from is provided diff --git a/src/pyqasm/maps/expressions.py b/src/pyqasm/maps/expressions.py index 4a1633d..0f19b07 100644 --- a/src/pyqasm/maps/expressions.py +++ b/src/pyqasm/maps/expressions.py @@ -17,7 +17,7 @@ """ -from typing import Callable, Union +from typing import Callable import numpy as np from openqasm3.ast import AngleType, BitType, BoolType, ComplexType, FloatType, IntType, UintType @@ -25,10 +25,10 @@ from pyqasm.exceptions import ValidationError # Define the type for the operator functions -OperatorFunction = Union[ - Callable[[Union[int, float, bool]], Union[int, float, bool]], - Callable[[Union[int, float, bool], Union[int, float, bool]], Union[int, float, bool]], -] +OperatorFunction = ( + Callable[[int | float | bool], int | float | bool] + | Callable[[int | float | bool, int | float | bool], int | float | bool] +) OPERATOR_MAP: dict[str, OperatorFunction] = { @@ -56,18 +56,18 @@ } -def qasm3_expression_op_map(op_name: str, *args) -> Union[float, int, bool]: +def qasm3_expression_op_map(op_name: str, *args) -> float | int | bool: """ Return the result of applying the given operator to the given operands. Args: op_name (str): The operator name. - *args: The operands of type Union[int, float, bool] + *args: The operands of type int | float | bool 1. For unary operators, a single operand (e.g., ~3) 2. For binary operators, two operands (e.g., 3 + 2) Returns: - (Union[float, int, bool]): The result of applying the operator to the operands. + (float | int | bool): The result of applying the operator to the operands. """ try: operator = OPERATOR_MAP[op_name] diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 685c44f..4933474 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -510,7 +510,7 @@ def validate(self, expand_traceback: Optional[bool] = False): if self._validated_program is True: return try: - os.environ["PYQASM_EXPAND_TRACEBACK"] = "1" if expand_traceback else "0" + os.environ["PYQASM_EXPAND_TRACEBACK"] = "true" if expand_traceback else "false" self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) self.accept(visitor) @@ -528,7 +528,7 @@ def unroll(self, **kwargs): unroll_barriers (bool): If True, barriers will be unrolled. Defaults to True. check_only (bool): If True, only check the program without executing it. Defaults to False. - expand_traceback (bool): If True, expand the traceback for better error messages. + expand_traceback (bool): If True, expand the traceback for verbose error messages. Raises: ValidationError: If the module fails validation during unrolling. @@ -542,7 +542,7 @@ def unroll(self, **kwargs): kwargs = {} try: os.environ["PYQASM_EXPAND_TRACEBACK"] = ( - "1" if kwargs.pop("expand_traceback", False) else "0" + "true" if kwargs.pop("expand_traceback", False) else "false" ) self.num_qubits, self.num_clbits = 0, 0 From 81e312567402eda46639c5bd45d929ca8fa567e4 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 16 Apr 2025 14:06:17 +0530 Subject: [PATCH 09/16] add lines and logs to tests --- src/pyqasm/transformer.py | 19 ++-- src/pyqasm/visitor.py | 11 ++- tests/qasm3/test_alias.py | 160 ++++++++++++++++++-------------- tests/qasm3/test_barrier.py | 16 +++- tests/qasm3/test_expressions.py | 35 +++++-- tests/qasm3/test_reset.py | 18 +++- tests/qasm3/test_sizeof.py | 61 ++++++------ tests/qasm3/test_switch.py | 60 ++++++++---- 8 files changed, 246 insertions(+), 134 deletions(-) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 6c159db..119ac35 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -34,6 +34,7 @@ ) from openqasm3.ast import IntType as Qasm3IntType from openqasm3.ast import ( + QASMNode, QuantumBarrier, QuantumGate, QuantumPhase, @@ -98,7 +99,9 @@ def update_array_element( multi_dim_arr[slicing] = value @staticmethod - def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: + def extract_values_from_discrete_set( + discrete_set: DiscreteSet, op_node: Optional[QASMNode] = None + ) -> list[int]: """Extract the values from a discrete set. Args: @@ -113,21 +116,25 @@ def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: raise_qasm3_error( f"Unsupported value '{Qasm3ExprEvaluator.evaluate_expression(value)[0]}' " "in discrete set", - error_node=discrete_set, - span=discrete_set.span, + error_node=op_node if op_node else discrete_set, + span=op_node.span if op_node else discrete_set.span, ) values.append(value.value) return values @staticmethod def get_qubits_from_range_definition( - range_def: RangeDefinition, qreg_size: int, is_qubit_reg: bool + range_def: RangeDefinition, + qreg_size: int, + is_qubit_reg: bool, + op_node: Optional[QASMNode] = None, ) -> list[int]: """Get the qubits from a range definition. Args: range_def (RangeDefinition): The range definition to get qubits from. qreg_size (int): The size of the register. is_qubit_reg (bool): Whether the register is a qubit register. + op_node (Optional[QASMNode]): The operation node. Returns: list[int]: The list of qubit identifiers. """ @@ -147,10 +154,10 @@ def get_qubits_from_range_definition( else Qasm3ExprEvaluator.evaluate_expression(range_def.step)[0] ) Qasm3Validator.validate_register_index( - start_qid, qreg_size, qubit=is_qubit_reg, op_node=range_def + start_qid, qreg_size, qubit=is_qubit_reg, op_node=op_node ) Qasm3Validator.validate_register_index( - end_qid - 1, qreg_size, qubit=is_qubit_reg, op_node=range_def + end_qid - 1, qreg_size, qubit=is_qubit_reg, op_node=op_node ) return list(range(start_qid, end_qid, step)) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index bee2f75..1d19706 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -410,10 +410,15 @@ def _get_op_bits( if isinstance(bit, qasm3_ast.IndexedIdentifier): if isinstance(bit.indices[0], qasm3_ast.DiscreteSet): - bit_ids = Qasm3Transformer.extract_values_from_discrete_set(bit.indices[0]) + bit_ids = Qasm3Transformer.extract_values_from_discrete_set( + bit.indices[0], operation + ) elif isinstance(bit.indices[0][0], qasm3_ast.RangeDefinition): bit_ids = Qasm3Transformer.get_qubits_from_range_definition( - bit.indices[0][0], reg_size_map[reg_name], is_qubit_reg=qubits + bit.indices[0][0], + reg_size_map[reg_name], + is_qubit_reg=qubits, + op_node=operation, ) else: bit_id = Qasm3ExprEvaluator.evaluate_expression(bit.indices[0][0])[0] @@ -1910,7 +1915,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[No alias_reg_size = aliased_reg_size elif isinstance(value, qasm3_ast.IndexExpression): if isinstance(value.index, qasm3_ast.DiscreteSet): # "let alias = q[{0,1}];" - qids = Qasm3Transformer.extract_values_from_discrete_set(value.index) + qids = Qasm3Transformer.extract_values_from_discrete_set(value.index, statement) for i, qid in enumerate(qids): Qasm3Validator.validate_register_index( qid, diff --git a/tests/qasm3/test_alias.py b/tests/qasm3/test_alias.py index 9774a2a..0ac8aad 100644 --- a/tests/qasm3/test_alias.py +++ b/tests/qasm3/test_alias.py @@ -119,7 +119,7 @@ def test_valid_alias_redefinition(): check_single_qubit_gate_op(result.unrolled_ast, 1, [2], "x") -def test_alias_wrong_indexing(): +def test_alias_wrong_indexing(caplog): """Test converting OpenQASM 3 program with wrong alias indexing.""" with pytest.raises( ValidationError, @@ -128,90 +128,110 @@ def test_alias_wrong_indexing(): "a comma-separated list of integers contained in braces {a,b,c,…}, or a range" ), ): - qasm3_alias_program = """ - OPENQASM 3.0; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; - qubit[5] q; + qubit[5] q; - let myqreg = q[1,2]; + let myqreg = q[1,2]; - x myqreg[0]; - """ - loads(qasm3_alias_program).validate() + x myqreg[0]; + """ + loads(qasm3_alias_program).validate() + assert "Error at line 7, column 12" in caplog.text + assert "let myqreg = q[1, 2];" in caplog.text -def test_alias_invalid_discrete_indexing(): + +def test_alias_invalid_discrete_indexing(caplog): """Test converting OpenQASM 3 program with invalid alias discrete indexing.""" with pytest.raises( ValidationError, match=r"Unsupported value .*", ): - qasm3_alias_program = """ - OPENQASM 3.0; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[5] q; - qubit[5] q; + let myqreg = q[{0.1}]; - let myqreg = q[{0.1}]; + x myqreg[0]; + """ + loads(qasm3_alias_program).validate() - x myqreg[0]; - """ - loads(qasm3_alias_program).validate() + assert "Error at line 7, column 12" in caplog.text + assert "let myqreg = q[{0.1}];" in caplog.text -def test_invalid_alias_redefinition(): +def test_invalid_alias_redefinition(caplog): """Test converting OpenQASM 3 program with redefined alias.""" with pytest.raises( ValidationError, match=re.escape(r"Re-declaration of variable 'alias'"), ): - qasm3_alias_program = """ - OPENQASM 3.0; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[5] q; + float[32] alias = 4.2; - qubit[5] q; - float[32] alias = 4.2; + let alias = q[2]; - let alias = q[2]; + x alias; + """ + loads(qasm3_alias_program).validate() - x alias; - """ - loads(qasm3_alias_program).validate() + assert "Error at line 8, column 12" in caplog.text + assert "let alias = q[2];" in caplog.text -def test_alias_defined_before(): +def test_alias_defined_before(caplog): """Test converting OpenQASM 3 program with alias defined before the qubit register.""" with pytest.raises( ValidationError, match=re.escape(r"Qubit register q2 not found for aliasing"), ): - qasm3_alias_program = """ - OPENQASM 3.0; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; - qubit[5] q1; + qubit[5] q1; - let myqreg = q2[1]; - """ - loads(qasm3_alias_program).validate() + let myqreg = q2[1]; + """ + loads(qasm3_alias_program).validate() + assert "Error at line 7, column 12" in caplog.text + assert "let myqreg = q2[1];" in caplog.text -def test_unsupported_alias(): + +def test_unsupported_alias(caplog): """Test converting OpenQASM 3 program with unsupported alias.""" with pytest.raises( ValidationError, match=r"Unsupported aliasing .*", ): - qasm3_alias_program = """ - OPENQASM 3.0; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[5] q; - qubit[5] q; + let myqreg = q[0] ++ q[1]; + """ + loads(qasm3_alias_program).validate() - let myqreg = q[0] ++ q[1]; - """ - loads(qasm3_alias_program).validate() + assert "Error at line 7, column 12" in caplog.text + assert "let myqreg = q[0] ++ q[1];" in caplog.text # def test_alias_in_scope_1(): @@ -279,32 +299,36 @@ def test_unsupported_alias(): # compare_reference_ir(result.bitcode, simple_file) -def test_alias_out_of_scope(): +def test_alias_out_of_scope(caplog): """Test converting OpenQASM 3 program with alias out of scope.""" with pytest.raises( ValidationError, match="Variable 'alias' not in scope for QuantumGate 'cx'", ): - qasm3_alias_program = """ - OPENQASM 3; - include "stdgates.inc"; - qubit[4] q; - bit[4] c; - - h q; - measure q -> c; - if(c[0]){ - let alias = q[0:2]; - x alias[0]; - cx alias[0], alias[1]; - } - - if(c[1] == 1){ - cx alias[1], q[2]; - } - - if(!c[2]){ - h q[2]; - } - """ - loads(qasm3_alias_program).validate() + with caplog.at_level("ERROR"): + qasm3_alias_program = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + + h q; + measure q -> c; + if(c[0]){ + let alias = q[0:2]; + x alias[0]; + cx alias[0], alias[1]; + } + + if(c[1] == 1){ + cx alias[1], q[2]; + } + + if(!c[2]){ + h q[2]; + } + """ + loads(qasm3_alias_program).validate() + + assert "Error at line 16, column 16" in caplog.text + assert "cx alias[1], q[2];" in caplog.text diff --git a/tests/qasm3/test_barrier.py b/tests/qasm3/test_barrier.py index 942a292..bbb1cb8 100644 --- a/tests/qasm3/test_barrier.py +++ b/tests/qasm3/test_barrier.py @@ -155,7 +155,7 @@ def test_unroll_barrier(): check_unrolled_qasm(dumps(module), expected_qasm) -def test_incorrect_barrier(): +def test_incorrect_barrier(caplog): undeclared = """ OPENQASM 3.0; @@ -168,7 +168,13 @@ def test_incorrect_barrier(): with pytest.raises( ValidationError, match="Missing qubit register declaration for 'q2' in QuantumBarrier" ): - loads(undeclared).validate() + with caplog.at_level("ERROR"): + loads(undeclared).validate() + + assert "Error at line 6, column 4" in caplog.text + assert "barrier q2;" in caplog.text + + caplog.clear() out_of_bounds = """ OPENQASM 3.0; @@ -181,4 +187,8 @@ def test_incorrect_barrier(): with pytest.raises( ValidationError, match="Index 3 out of range for register of size 2 in qubit" ): - loads(out_of_bounds).validate() + with caplog.at_level("ERROR"): + loads(out_of_bounds).validate() + + assert "Error at line 6, column 4" in caplog.text + assert "barrier q1[:4];" in caplog.text diff --git a/tests/qasm3/test_expressions.py b/tests/qasm3/test_expressions.py index 88ce351..c5034ea 100644 --- a/tests/qasm3/test_expressions.py +++ b/tests/qasm3/test_expressions.py @@ -74,18 +74,41 @@ def test_bit_in_expression(): check_measure_op(result.unrolled_ast, 1, meas_pairs) -def test_incorrect_expressions(): +def test_incorrect_expressions(caplog): with pytest.raises(ValidationError, match=r"Unsupported expression type .*"): - loads("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;").validate() + with caplog.at_level("ERROR"): + loads("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;").validate() + assert "Error at line 1, column 32" in caplog.text + assert "32.0im" in caplog.text + + caplog.clear() with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): - loads("OPENQASM 3; qubit q; rx(~1.3) q;").validate() + with caplog.at_level("ERROR"): + loads("OPENQASM 3; qubit q; rx(~1.3) q;").validate() + assert "Error at line 1" in caplog.text + assert "~1.3" in caplog.text + + caplog.clear() with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): - loads("OPENQASM 3; qubit q; rx(~1.3+5im) q;").validate() + with caplog.at_level("ERROR"): + loads("OPENQASM 3; qubit q; rx(~1.3+5im) q;").validate() + assert "Error at line 1" in caplog.text + assert "~1.3" in caplog.text + + caplog.clear() with pytest.raises(ValidationError, match="Undefined identifier 'x' in expression"): - loads("OPENQASM 3; qubit q; rx(x) q;").validate() + with caplog.at_level("ERROR"): + loads("OPENQASM 3; qubit q; rx(x) q;").validate() + assert "Error at line 1" in caplog.text + assert "x" in caplog.text + + caplog.clear() with pytest.raises(ValidationError, match="Uninitialized variable 'x' in expression"): - loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() + with caplog.at_level("ERROR"): + loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() + assert "Error at line 1" in caplog.text + assert "x" in caplog.text diff --git a/tests/qasm3/test_reset.py b/tests/qasm3/test_reset.py index 44ed8a6..dafe71f 100644 --- a/tests/qasm3/test_reset.py +++ b/tests/qasm3/test_reset.py @@ -83,7 +83,7 @@ def my_function(qubit a) { check_unrolled_qasm(dumps(result), expected_qasm) -def test_incorrect_resets(): +def test_incorrect_resets(caplog): undeclared = """ OPENQASM 3.0; include "stdgates.inc"; @@ -94,7 +94,11 @@ def test_incorrect_resets(): reset q2[0]; """ with pytest.raises(ValidationError): - loads(undeclared).validate() + with caplog.at_level("ERROR"): + loads(undeclared).validate() + + assert "Error at line 8, column 4" in caplog.text + assert "reset q2[0]" in caplog.text index_error = """ OPENQASM 3.0; @@ -105,5 +109,11 @@ def test_incorrect_resets(): // out of bounds reset q1[4]; """ - with pytest.raises(ValidationError): - loads(index_error).validate() + with pytest.raises( + ValidationError, match=r"Index 4 out of range for register of size 2 in qubit" + ): + with caplog.at_level("ERROR"): + loads(index_error).validate() + + assert "Error at line 8, column 4" in caplog.text + assert "reset q1[4]" in caplog.text diff --git a/tests/qasm3/test_sizeof.py b/tests/qasm3/test_sizeof.py index fc7b8a6..7a501bd 100644 --- a/tests/qasm3/test_sizeof.py +++ b/tests/qasm3/test_sizeof.py @@ -79,47 +79,56 @@ def test_sizeof_multiple_types(): check_single_qubit_rotation_op(result.unrolled_ast, 2, [1, 1], [2, 3], "rx") -def test_unsupported_target(): +def test_unsupported_target(caplog): """Test sizeof over index expressions""" with pytest.raises(ValidationError, match=r"Unsupported target type .*"): - qasm3_string = """ - OPENQASM 3; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; - array[int[32], 3, 2] my_ints; + array[int[32], 3, 2] my_ints; - int[32] size1 = sizeof(my_ints[0]); // this is invalid - """ - loads(qasm3_string).validate() + int[32] size1 = sizeof(my_ints[0]); // this is invalid + """ + loads(qasm3_string).validate() + assert "Error at line 7, column 28" in caplog.text + assert "sizeof(my_ints[0])" in caplog.text -def test_sizeof_on_non_array(): +def test_sizeof_on_non_array(caplog): """Test sizeof on a non-array""" with pytest.raises( ValidationError, match="Invalid sizeof usage, variable 'my_int' is not an array." ): - qasm3_string = """ - OPENQASM 3; - include "stdgates.inc"; + with caplog.at_level("ERROR"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; - int[32] my_int = 3; + int[32] my_int = 3; - int[32] size1 = sizeof(my_int); // this is invalid - """ - loads(qasm3_string).validate() + int[32] size1 = sizeof(my_int); // this is invalid + """ + loads(qasm3_string).validate() + assert "Error at line 7, column 28" in caplog.text + assert "sizeof(my_int)" in caplog.text -def test_out_of_bounds_reference(): +def test_out_of_bounds_reference(caplog): """Test sizeof on an out of bounds reference""" with pytest.raises( ValidationError, match="Index 3 out of bounds for array 'my_ints' with 2 dimensions" ): - qasm3_string = """ - OPENQASM 3; - include "stdgates.inc"; - - array[int[32], 3, 2] my_ints; - - int[32] size1 = sizeof(my_ints, 3); // this is invalid - """ - loads(qasm3_string).validate() + with caplog.at_level("ERROR"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + array[int[32], 3, 2] my_ints; + + int[32] size1 = sizeof(my_ints, 3); // this is invalid + """ + loads(qasm3_string).validate() + assert "Error at line 7, column 28" in caplog.text + assert "sizeof(my_ints, 3)" in caplog.text diff --git a/tests/qasm3/test_switch.py b/tests/qasm3/test_switch.py index 71b9219..0f90d64 100644 --- a/tests/qasm3/test_switch.py +++ b/tests/qasm3/test_switch.py @@ -275,7 +275,7 @@ def my_function(qubit q, float[32] b) { @pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) -def test_invalid_scalar_switch_target(invalid_type): +def test_invalid_scalar_switch_target(invalid_type, caplog): """Test that switch raises error if target is not an integer.""" base_invalid_program = ( @@ -300,12 +300,16 @@ def test_invalid_scalar_switch_target(invalid_type): ) with pytest.raises(ValidationError, match=re.escape("Switch target i must be of type int")): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + + assert "Error at line 8, column 4" in caplog.text + assert "switch (i)" in caplog.text @pytest.mark.parametrize("invalid_type", ["float", "bool"]) -def test_invalid_array_switch_target(invalid_type): +def test_invalid_array_switch_target(invalid_type, caplog): """Test that switch raises error if target is array element and not an integer.""" base_invalid_program = ( @@ -330,15 +334,19 @@ def test_invalid_array_switch_target(invalid_type): ) with pytest.raises(ValidationError, match=re.escape("Switch target i must be of type int")): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + + assert "Error at line 8, column 4" in caplog.text + assert "switch (i[0][1])" in caplog.text @pytest.mark.parametrize( "invalid_stmt", ["def test1() { int i = 1; }", "array[int[32], 3, 2] arr_int;", "gate test_1() q { h q;}"], ) -def test_unsupported_statements_in_case(invalid_stmt): +def test_unsupported_statements_in_case(invalid_stmt, caplog): """Test that switch raises error if invalid statements are present in the case block""" base_invalid_program = ( @@ -363,11 +371,15 @@ def test_unsupported_statements_in_case(invalid_stmt): """ ) with pytest.raises(ValidationError, match=r"Unsupported statement .*"): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + + assert "Error at line 11, column 4" in caplog.text + assert invalid_stmt.split()[0] in caplog.text # only test for def / array / gate keywords -def test_non_int_expression_case(): +def test_non_int_expression_case(caplog): """Test that switch raises error if case expression is not an integer.""" base_invalid_program = """ @@ -390,11 +402,15 @@ def test_non_int_expression_case(): ValidationError, match=r"Invalid value 4.3 with type .* for required type ", ): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + + assert "Error at line 8, column 13" in caplog.text + assert "4.3" in caplog.text -def test_non_int_variable_expression(): +def test_non_int_variable_expression(caplog): """Test that switch raises error if case expression has a non-int variable in expression.""" @@ -418,11 +434,15 @@ def test_non_int_variable_expression(): ValidationError, match=r"Invalid type .* of variable 'f' for required type ", ): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + assert "Error at line 9, column 13" in caplog.text + assert "f" in caplog.text -def test_non_constant_expression_case(): + +def test_non_constant_expression_case(caplog): """Test that switch raises error if case expression is not a constant.""" base_invalid_program = """ @@ -446,5 +466,9 @@ def test_non_constant_expression_case(): with pytest.raises( ValidationError, match=r"Expected variable .* to be constant in given expression" ): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + with caplog.at_level("ERROR"): + qasm3_switch_program = base_invalid_program + loads(qasm3_switch_program).validate() + + assert "Error at line 10, column 13 in QASM file" in caplog.text + assert "j" in caplog.text From 83fdb97190b773d605e1c7ec84f7f3963e97cb14 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 17 Apr 2025 12:56:19 +0530 Subject: [PATCH 10/16] more updates on tests --- src/pyqasm/maps/gates.py | 21 ++- src/pyqasm/visitor.py | 17 ++- tests/qasm3/resources/gates.py | 49 ++++++- tests/qasm3/test_expressions.py | 10 +- tests/qasm3/test_gates.py | 49 +++++-- tests/qasm3/test_if.py | 230 ++++++++++++++++---------------- tests/qasm3/test_loop.py | 36 ++--- tests/qasm3/test_measurement.py | 160 ++++++++++++---------- tests/qasm3/test_switch.py | 169 +++++++++++------------ 9 files changed, 421 insertions(+), 320 deletions(-) diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index 1de2da8..999c482 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -23,10 +23,16 @@ from typing import Callable import numpy as np -from openqasm3.ast import FloatLiteral, Identifier, IndexedIdentifier, QuantumGate, QuantumPhase +from openqasm3.ast import ( + FloatLiteral, + Identifier, + IndexedIdentifier, + QuantumGate, + QuantumPhase, +) from pyqasm.elements import BasisSet, InversionOp -from pyqasm.exceptions import ValidationError +from pyqasm.exceptions import ValidationError, raise_qasm3_error from pyqasm.linalg import kak_decomposition_angles from pyqasm.maps.expressions import CONSTANTS_MAP @@ -1168,12 +1174,13 @@ def map_qasm_op_num_params(op_name: str) -> int: return 0 -def map_qasm_op_to_callable(op_name: str) -> tuple[Callable, int]: +# pylint: disable-next=inconsistent-return-statements +def map_qasm_op_to_callable(op_node: QuantumGate) -> tuple[Callable, int]: # type: ignore[return] """ Map a QASM operation to a callable. Args: - op_name (str): The QASM operation name. + op_node (QuantumGate): The QASM operation. Returns: tuple: A tuple containing the callable and the number of qubits the operation acts on. @@ -1181,6 +1188,8 @@ def map_qasm_op_to_callable(op_name: str) -> tuple[Callable, int]: Raises: ValidationError: If the QASM operation is unsupported or undeclared. """ + op_name = op_node.name.name + op_maps: list[tuple[dict, int]] = [ (ONE_QUBIT_OP_MAP, 1), (ONE_QUBIT_ROTATION_MAP, 1), @@ -1196,7 +1205,9 @@ def map_qasm_op_to_callable(op_name: str) -> tuple[Callable, int]: except KeyError: continue - raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") + raise_qasm3_error( + f"Unsupported / undeclared QASM operation: {op_name}", error_node=op_node, span=op_node.span + ) SELF_INVERTING_ONE_QUBIT_OP_SET = {"id", "h", "x", "y", "z"} diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 1d19706..eeae214 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -26,6 +26,7 @@ import numpy as np import openqasm3.ast as qasm3_ast +from openqasm3.printer import dumps from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import ClbitDepthNode, Context, InversionOp, QubitDepthNode, Variable @@ -642,8 +643,16 @@ def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: """ param_list = [] for param in operation.arguments: - param_value = Qasm3ExprEvaluator.evaluate_expression(param)[0] - param_list.append(param_value) + try: + param_value = Qasm3ExprEvaluator.evaluate_expression(param)[0] + param_list.append(param_value) + except ValidationError as err: + raise_qasm3_error( + f"Invalid parameter '{dumps(param)}' for gate '{operation.name.name}'", + error_node=operation, + span=operation.span, + raised_from=err, + ) return param_list @@ -788,7 +797,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ) op_qubit_count = op_qubit_total_count - len(ctrls) else: - qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) + qasm_func, op_qubit_count = map_qasm_op_to_callable(operation) else: # in basic gates, inverse action only affects the rotation gates qasm_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_callable( @@ -965,7 +974,7 @@ def _visit_external_gate_operation( # Ignore result, this is just for validation self._visit_basic_gate_operation(operation) # Don't need to check if basic gate exists, since we just validated the call - _, gate_qubit_count = map_qasm_op_to_callable(operation.name.name) + _, gate_qubit_count = map_qasm_op_to_callable(operation) op_parameters = [ qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation) diff --git a/tests/qasm3/resources/gates.py b/tests/qasm3/resources/gates.py index c177f94..9cac401 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -256,6 +256,9 @@ def test_fixture(): h q2; // undeclared register """, "Missing qubit register declaration for 'q2' in QuantumGate", + 6, + 8, + "h q2;", ), "undeclared_1qubit_op": ( """ @@ -266,6 +269,9 @@ def test_fixture(): u_abc(0.5, 0.5, 0.5) q1; // unsupported gate """, "Unsupported / undeclared QASM operation: u_abc", + 6, + 8, + "u_abc(0.5, 0.5, 0.5) q1", ), "undeclared_1qubit_op_with_indexing": ( """ @@ -277,6 +283,9 @@ def test_fixture(): u_abc(0.5, 0.5, 0.5) q1[0], q1[1]; // unsupported gate """, "Unsupported / undeclared QASM operation: u_abc", + 7, + 8, + "u_abc(0.5, 0.5, 0.5) q1[0], q1[1];", ), "undeclared_3qubit_op": ( """ @@ -287,6 +296,9 @@ def test_fixture(): u_abc(0.5, 0.5, 0.5) q1[0], q1[1], q1[2]; // unsupported gate """, "Unsupported / undeclared QASM operation: u_abc", + 6, + 8, + "u_abc(0.5, 0.5, 0.5) q1[0], q1[1], q1[2];", ), "invalid_gate_application": ( """ @@ -297,6 +309,9 @@ def test_fixture(): cx q1; // invalid application of gate, as we apply it to 3 qubits in blocks of 2 """, "Invalid number of qubits 3 for operation cx", + 6, + 8, + "cx q1[0], q1[1], q1[2];", # expanded line ), "unsupported_parameter_type": ( """ @@ -304,9 +319,12 @@ def test_fixture(): include "stdgates.inc"; qubit[2] q1; - rx(a) q1; // unsupported parameter type + rx(a) q1; """, - "Undefined identifier 'a' in.*", + "Invalid parameter 'a' for .*", + 6, + 11, + "rx(a) q1[0], q1[1];", # expanded line ), "duplicate_qubits": ( """ @@ -317,6 +335,9 @@ def test_fixture(): cx q1[0] , q1[0]; // duplicate qubit """, r"Duplicate qubit 'q1\[0\]' arg in gate cx", + 6, + 8, + "cx q1[0], q1[0];", ), } @@ -329,6 +350,9 @@ def test_fixture(): gphase(pi) q; """, r"Qubit arguments not allowed for 'gphase' operation", + 4, + 8, + "gphase(3.141592653589793) q[0];", ), "undeclared_custom": ( """ @@ -339,6 +363,9 @@ def test_fixture(): custom_gate q1; // undeclared gate """, "Unsupported / undeclared QASM operation: custom_gate", + 6, + 8, + "custom_gate q1[0], q1[1];", # expanded line ), "parameter_mismatch_1": ( """ @@ -354,6 +381,9 @@ def test_fixture(): custom_gate(0.5) q1; // parameter count mismatch """, "Parameter count mismatch for gate 'custom_gate'. Expected 2 arguments, but got 1 instead.", + 11, + 8, + "custom_gate(0.5) q1[0], q1[1];", # expanded line ), "parameter_mismatch_2": ( """ @@ -368,6 +398,9 @@ def test_fixture(): rz(0.5, 0.0) q[0]; """, "Expected 1 parameter for gate 'rz', but got 2", + 10, + 8, + "rz(0.5, 0.0) q[0];", ), "qubit_mismatch": ( """ @@ -383,6 +416,9 @@ def test_fixture(): custom_gate(0.5, 0.5) q1; // qubit count mismatch """, "Qubit count mismatch for gate 'custom_gate'. Expected 2 qubits, but got 3 instead.", + 11, + 8, + "custom_gate(0.5, 0.5) q1[0], q1[1], q1[2];", # expanded line ), "indexing_not_supported": ( """ @@ -398,6 +434,9 @@ def test_fixture(): custom_gate(0.5, 0.5) q1; // indexing not supported """, "Indexing .* not supported in gate definition", + 7, + 18, + "ry(0.5) q[0];", # expanded line ), "recursive_definition": ( """ @@ -412,6 +451,9 @@ def test_fixture(): custom_gate(0.5, 0.5) q1; // recursive definition """, "Recursive definitions not allowed .*", + 6, + 12, + "custom_gate(a, b) p, q;", ), "duplicate_definition": ( """ @@ -432,5 +474,8 @@ def test_fixture(): custom_gate(0.5, 0.5) q1; // duplicate definition """, "Duplicate quantum gate definition for 'custom_gate'", + 10, + 8, + "gate custom_gate(a, b) p, q {", ), } diff --git a/tests/qasm3/test_expressions.py b/tests/qasm3/test_expressions.py index c5034ea..9f023c5 100644 --- a/tests/qasm3/test_expressions.py +++ b/tests/qasm3/test_expressions.py @@ -75,7 +75,7 @@ def test_bit_in_expression(): def test_incorrect_expressions(caplog): - with pytest.raises(ValidationError, match=r"Unsupported expression type .*"): + with pytest.raises(ValidationError, match=r"Invalid parameter .*"): with caplog.at_level("ERROR"): loads("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;").validate() assert "Error at line 1, column 32" in caplog.text @@ -83,7 +83,7 @@ def test_incorrect_expressions(caplog): caplog.clear() - with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): + with pytest.raises(ValidationError, match=r"Invalid parameter .*"): with caplog.at_level("ERROR"): loads("OPENQASM 3; qubit q; rx(~1.3) q;").validate() assert "Error at line 1" in caplog.text @@ -91,7 +91,7 @@ def test_incorrect_expressions(caplog): caplog.clear() - with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): + with pytest.raises(ValidationError, match=r"Invalid parameter .*"): with caplog.at_level("ERROR"): loads("OPENQASM 3; qubit q; rx(~1.3+5im) q;").validate() assert "Error at line 1" in caplog.text @@ -99,7 +99,7 @@ def test_incorrect_expressions(caplog): caplog.clear() - with pytest.raises(ValidationError, match="Undefined identifier 'x' in expression"): + with pytest.raises(ValidationError, match="Invalid parameter 'x' .*"): with caplog.at_level("ERROR"): loads("OPENQASM 3; qubit q; rx(x) q;").validate() assert "Error at line 1" in caplog.text @@ -107,7 +107,7 @@ def test_incorrect_expressions(caplog): caplog.clear() - with pytest.raises(ValidationError, match="Uninitialized variable 'x' in expression"): + with pytest.raises(ValidationError, match="Invalid parameter 'x' .*"): with caplog.at_level("ERROR"): loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() assert "Error at line 1" in caplog.text diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 7df20f4..c0e88a5 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -222,13 +222,6 @@ def test_qasm_u2_gates(): check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [0.5, 0.5], "u2") -@pytest.mark.parametrize("test_name", SINGLE_QUBIT_GATE_INCORRECT_TESTS.keys()) -def test_incorrect_single_qubit_gates(test_name): - qasm_input, error_message = SINGLE_QUBIT_GATE_INCORRECT_TESTS[test_name] - with pytest.raises(ValidationError, match=error_message): - loads(qasm_input).validate() - - @pytest.mark.parametrize("test_name", custom_op_tests) def test_custom_ops(test_name, request): qasm3_string = request.getfixturevalue(test_name) @@ -599,6 +592,9 @@ def test_nested_gate_modifiers(): ctrl(b+1) @ x q[0], q[1]; """, "Controlled modifier arguments must be compile-time constants.*", + 8, + 4, + "ctrl(b + 1) @ x q[0], q[1];", ), ( """ @@ -608,6 +604,9 @@ def test_nested_gate_modifiers(): ctrl(1.5) @ x q[0], q[1]; """, "Controlled modifier argument must be a positive integer.*", + 5, + 4, + "ctrl(1.5) @ x q[0], q[1];", ), ( """ @@ -617,17 +616,41 @@ def test_nested_gate_modifiers(): pow(1.5) @ x q; """, "Power modifier argument must be an integer.*", + 5, + 4, + "pow(1.5) @ x q[0];", ), ], ) -def test_modifier_arg_error(test): - qasm3_string, error_message = test +def test_modifier_arg_error(test, caplog): + qasm3_string, error_message, line_num, col_num, line = test with pytest.raises(ValidationError, match=error_message): - loads(qasm3_string).validate() + with caplog.at_level("ERROR"): + loads(qasm3_string).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert line in caplog.text @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) -def test_incorrect_custom_ops(test_name): - qasm_input, error_message = CUSTOM_GATE_INCORRECT_TESTS[test_name] +def test_incorrect_custom_ops(test_name, caplog): + qasm_input, error_message, line_num, col_num, line = CUSTOM_GATE_INCORRECT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - loads(qasm_input).validate() + with caplog.at_level("ERROR"): + loads(qasm_input).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert line in caplog.text + + +@pytest.mark.parametrize("test_name", SINGLE_QUBIT_GATE_INCORRECT_TESTS.keys()) +def test_incorrect_single_qubit_gates(test_name, caplog): + qasm_input, error_message, line_num, col_num, line = SINGLE_QUBIT_GATE_INCORRECT_TESTS[ + test_name + ] + with pytest.raises(ValidationError, match=error_message): + with caplog.at_level("ERROR"): + loads(qasm_input).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert line in caplog.text diff --git a/tests/qasm3/test_if.py b/tests/qasm3/test_if.py index e3eeec8..5a06798 100644 --- a/tests/qasm3/test_if.py +++ b/tests/qasm3/test_if.py @@ -206,131 +206,133 @@ def test_multi_bit_if(): check_unrolled_qasm(dumps(result), expected_qasm) -def test_incorrect_if(): - - with pytest.raises(ValidationError, match=r"Missing if block"): - loads( +@pytest.mark.parametrize( + "qasm_code,error_message,line_num,col_num,err_line", + [ + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c[0]){ - } - """ - ).validate() - - with pytest.raises(ValidationError, match=r"Undefined identifier 'c2' in expression"): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c[0]){ + } + """, + r"Missing if block", + 8, + 12, + "if (c[0]) {", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c2[0]){ - cx q; - } - """ - ).validate() - - with pytest.raises(ValidationError, match=r"Only '!' supported .*"): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c2[0]){ + cx q; + } + """, + r"Undefined identifier 'c2' in expression", + 8, + 15, + "c2[0]", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(~c[0]){ - cx q; - } - """ - ).validate() - with pytest.raises( - ValidationError, - match=r"Only {==, >=, <=, >, <} supported in branching condition with classical register", - ): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(~c[0]){ + cx q; + } + """, + r"Only '!' supported .*", + 8, + 15, + "~c[0]", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c[0] >> 1){ - cx q; - } - """ - ).validate() - with pytest.raises( - ValidationError, - match=r"Only simple comparison supported .*", - ): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c[0] >> 1){ + cx q; + } + """, + r"Only {==, >=, <=, >, <} supported in branching condition with classical register", + 8, + 15, + "c[0] >> 1", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c){ - cx q; - } - """ - ).validate() - with pytest.raises( - ValidationError, - match=r"RangeDefinition not supported in branching condition", - ): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c){ + cx q; + } + """, + r"Only simple comparison supported .*", + 8, + 15, + "c", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c[0:1]){ - cx q; - } - """ - ).validate() - - with pytest.raises( - ValidationError, - match=r"DiscreteSet not supported in branching condition", - ): - loads( + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c[0:1]){ + cx q; + } + """, + r"RangeDefinition not supported in branching condition", + 8, + 15, + "c[0:1]", + ), + ( """ OPENQASM 3.0; - include "stdgates.inc"; - qubit[2] q; - bit[2] c; - - h q; - measure q->c; - - if(c[{0,1}]){ - cx q; - } - """ - ).validate() + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q; + measure q->c; + if(c[{0,1}]){ + cx q; + } + """, + r"DiscreteSet not supported in branching condition", + 8, + 15, + "c[{0, 1}]", + ), + ], +) # pylint: disable-next= too-many-arguments +def test_incorrect_if(qasm_code, error_message, line_num, col_num, err_line, caplog): + with pytest.raises(ValidationError, match=error_message): + with caplog.at_level("ERROR"): + loads(qasm_code).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text diff --git a/tests/qasm3/test_loop.py b/tests/qasm3/test_loop.py index d0fcb82..ad7a330 100644 --- a/tests/qasm3/test_loop.py +++ b/tests/qasm3/test_loop.py @@ -303,7 +303,7 @@ def my_function_2(qubit q2, int[32] b){ check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 0, 0], [0, 3, 6], "rx") -def test_convert_qasm3_for_loop_unsupported_type(): +def test_convert_qasm3_for_loop_unsupported_type(caplog): """Test correct error when converting a QASM3 program that contains a for loop initialized from an unsupported type.""" with pytest.raises( @@ -313,18 +313,22 @@ def test_convert_qasm3_for_loop_unsupported_type(): " of set_declaration in loop." ), ): - loads( - """ - OPENQASM 3.0; - include "stdgates.inc"; - - qubit[4] q; - bit[4] c; - - h q; - for bit b in "001" { - x q[b]; - } - measure q->c; - """, - ).validate() + with caplog.at_level("ERROR"): + loads( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[4] q; + bit[4] c; + + h q; + for bit b in "001" { + x q[b]; + } + measure q->c; + """, + ).validate() + + assert "Error at line 9, column 16" in caplog.text + assert 'for bit b in "001"' in caplog.text diff --git a/tests/qasm3/test_measurement.py b/tests/qasm3/test_measurement.py index 0830cec..61ace6e 100644 --- a/tests/qasm3/test_measurement.py +++ b/tests/qasm3/test_measurement.py @@ -189,75 +189,95 @@ def test_standalone_measurement(): check_unrolled_qasm(dumps(module), expected_qasm) -def test_incorrect_measure(): - def run_test(qasm3_code, error_message): - with pytest.raises(ValidationError, match=error_message): +@pytest.mark.parametrize( + "qasm3_code,error_message,line_num,col_num,err_line", + [ + # Test for undeclared register q2 + ( + """ + OPENQASM 3.0; + qubit[2] q1; + bit[2] c1; + c1[0] = measure q2[0]; // undeclared register + """, + r"Missing register declaration for 'q2' .*", + 5, + 12, + "c1[0] = measure q2[0];", + ), + # Test for undeclared register c2 + ( + """ + OPENQASM 3.0; + qubit[2] q1; + bit[2] c1; + measure q1 -> c2; // undeclared register + """, + r"Missing register declaration for 'c2' .*", + 5, + 12, + "c2 = measure q1;", + ), + # Test for size mismatch between q1 and c2 + ( + """ + OPENQASM 3.0; + qubit[2] q1; + bit[2] c1; + bit[1] c2; + c2 = measure q1; // size mismatch + """, + r"Register sizes of q1 and c2 do not match .*", + 6, + 12, + "c2 = measure q1;", + ), + # Test for size mismatch between q1 and c1 in ranges + ( + """ + OPENQASM 3.0; + qubit[5] q1; + bit[4] c1; + bit[1] c2; + c1[:3] = measure q1; // size mismatch + """, + r"Register sizes of q1 and c1 do not match .*", + 6, + 12, + "c1[:3] = measure q1;", + ), + # Test for out of bounds index for q1 + ( + """ + OPENQASM 3.0; + qubit[2] q1; + bit[2] c1; + measure q1[3] -> c1[0]; // out of bounds + """, + r"Index 3 out of range for register of size 2 in qubit", + 5, + 12, + "c1[0] = measure q1[3];", + ), + # Test for out of bounds index for c1 + ( + """ + OPENQASM 3.0; + qubit[2] q1; + bit[2] c1; + measure q1 -> c1[3]; // out of bounds + """, + r"Index 3 out of range for register of size 2 in clbit", + 5, + 12, + "c1[3] = measure q1;", + ), + ], +) # pylint: disable-next= too-many-arguments +def test_incorrect_measure(qasm3_code, error_message, line_num, col_num, err_line, caplog): + with pytest.raises(ValidationError, match=error_message): + with caplog.at_level(level="ERROR"): loads(qasm3_code).validate() - # Test for undeclared register q2 - run_test( - """ - OPENQASM 3.0; - qubit[2] q1; - bit[2] c1; - c1[0] = measure q2[0]; // undeclared register - """, - r"Missing register declaration for 'q2' .*", - ) - - # Test for undeclared register c2 - run_test( - """ - OPENQASM 3.0; - qubit[2] q1; - bit[2] c1; - measure q1 -> c2; // undeclared register - """, - r"Missing register declaration for 'c2' .*", - ) - - # Test for size mismatch between q1 and c2 - run_test( - """ - OPENQASM 3.0; - qubit[2] q1; - bit[2] c1; - bit[1] c2; - c2 = measure q1; // size mismatch - """, - r"Register sizes of q1 and c2 do not match .*", - ) - - # Test for size mismatch between q1 and c2 in ranges - run_test( - """ - OPENQASM 3.0; - qubit[5] q1; - bit[4] c1; - bit[1] c2; - c1[:3] = measure q1; // size mismatch - """, - r"Register sizes of q1 and c1 do not match .*", - ) - - # Test for out of bounds index for q1 - run_test( - """ - OPENQASM 3.0; - qubit[2] q1; - bit[2] c1; - measure q1[3] -> c1[0]; // out of bounds - """, - r"Index 3 out of range for register of size 2 in qubit", - ) - - # Test for out of bounds index for c1 - run_test( - """ - OPENQASM 3.0; - qubit[2] q1; - bit[2] c1; - measure q1 -> c1[3]; // out of bounds - """, - r"Index 3 out of range for register of size 2 in clbit", - ) + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text diff --git a/tests/qasm3/test_switch.py b/tests/qasm3/test_switch.py index 0f90d64..6f362c1 100644 --- a/tests/qasm3/test_switch.py +++ b/tests/qasm3/test_switch.py @@ -379,96 +379,83 @@ def test_unsupported_statements_in_case(invalid_stmt, caplog): assert invalid_stmt.split()[0] in caplog.text # only test for def / array / gate keywords -def test_non_int_expression_case(caplog): - """Test that switch raises error if case expression is not an integer.""" - - base_invalid_program = """ - OPENQASM 3.0; - include "stdgates.inc"; - const int i = 4; - qubit q; - - switch(i) { - case 4.3, 2 { - x q; - } - default { - z q; - } - } - """ - - with pytest.raises( - ValidationError, - match=r"Invalid value 4.3 with type .* for required type ", - ): - with caplog.at_level("ERROR"): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() - - assert "Error at line 8, column 13" in caplog.text - assert "4.3" in caplog.text - - -def test_non_int_variable_expression(caplog): - """Test that switch raises error if case expression has a non-int - variable in expression.""" - - base_invalid_program = """ - OPENQASM 3.0; - include "stdgates.inc"; - const int i = 4; - const float f = 4.0; - qubit q; - - switch(i) { - case f, 2 { - x q; - } - default { - z q; - } - } - """ - with pytest.raises( - ValidationError, - match=r"Invalid type .* of variable 'f' for required type ", - ): - with caplog.at_level("ERROR"): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() - - assert "Error at line 9, column 13" in caplog.text - assert "f" in caplog.text - - -def test_non_constant_expression_case(caplog): - """Test that switch raises error if case expression is not a constant.""" - - base_invalid_program = """ - OPENQASM 3.0; - include "stdgates.inc"; - int i = 4; - qubit q; - int j = 3; - int k = 2; - - switch(i) { - case j + k { - x q; - } - default { - z q; - } - } - """ - - with pytest.raises( - ValidationError, match=r"Expected variable .* to be constant in given expression" - ): +@pytest.mark.parametrize( + "qasm3_code,error_message,line_num,col_num,err_line", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + const int i = 4; + qubit q; + + switch(i) { + case 4.3, 2 { + x q; + } + default { + z q; + } + } + """, + r"Invalid value 4.3 with type .* for required type ", + 8, + 21, + "4.3", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + const int i = 4; + const float f = 4.0; + qubit q; + + switch(i) { + case f, 2 { + x q; + } + default { + z q; + } + } + """, + r"Invalid type .* of variable 'f' for required type ", + 9, + 21, + "f", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int i = 4; + qubit q; + int j = 3; + int k = 2; + + switch(i) { + case j + k { + x q; + } + default { + z q; + } + } + """, + r"Expected variable .* to be constant in given expression", + 10, + 21, + "j", + ), + ], +) # pylint: disable-next= too-many-arguments +def test_switch_case_errors(qasm3_code, error_message, line_num, col_num, err_line, caplog): + """Test that switch raises appropriate errors for various invalid case conditions.""" + + with pytest.raises(ValidationError, match=error_message): with caplog.at_level("ERROR"): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() + loads(qasm3_code).validate() - assert "Error at line 10, column 13 in QASM file" in caplog.text - assert "j" in caplog.text + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text From f7724c356a812a68d42450f0a4f6f43c507baee0 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 17 Apr 2025 13:46:02 +0530 Subject: [PATCH 11/16] remove env var set --- src/pyqasm/exceptions.py | 8 ++++++-- src/pyqasm/modules/base.py | 9 +-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 8fb6ce8..5561f9a 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -97,8 +97,12 @@ def raise_qasm3_error( if error_parts: logger.error("\n".join(error_parts)) - if os.environ.get("PYQASM_EXPAND_TRACEBACK") == "false": - sys.tracebacklimit = 0 # Disable traceback for cleaner output + if os.getenv("PYQASM_EXPAND_TRACEBACK", "false") == "false": + # Disable traceback for cleaner output + sys.tracebacklimit = 0 + else: + # default value + sys.tracebacklimit = None # type: ignore # Extract the latest message from the traceback if raised_from is provided if raised_from: diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 4933474..5b6890c 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -16,7 +16,6 @@ Definition of the base Qasm module """ -import os from abc import ABC, abstractmethod from copy import deepcopy from typing import Optional @@ -505,12 +504,11 @@ def reverse_qubit_order(self, in_place=True): # 4. return the module return qasm_module - def validate(self, expand_traceback: Optional[bool] = False): + def validate(self): """Validate the module""" if self._validated_program is True: return try: - os.environ["PYQASM_EXPAND_TRACEBACK"] = "true" if expand_traceback else "false" self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) self.accept(visitor) @@ -528,7 +526,6 @@ def unroll(self, **kwargs): unroll_barriers (bool): If True, barriers will be unrolled. Defaults to True. check_only (bool): If True, only check the program without executing it. Defaults to False. - expand_traceback (bool): If True, expand the traceback for verbose error messages. Raises: ValidationError: If the module fails validation during unrolling. @@ -541,10 +538,6 @@ def unroll(self, **kwargs): if not kwargs: kwargs = {} try: - os.environ["PYQASM_EXPAND_TRACEBACK"] = ( - "true" if kwargs.pop("expand_traceback", False) else "false" - ) - self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(module=self, **kwargs) self.accept(visitor) From 10e762071fff8adf437e3f8bd74b25e44b744934 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 17 Apr 2025 15:03:36 +0530 Subject: [PATCH 12/16] fix up logging --- src/pyqasm/_logging.py | 33 +++++++++++++++++++++++++++++++++ src/pyqasm/cli/validate.py | 1 + src/pyqasm/exceptions.py | 9 +-------- src/pyqasm/visitor.py | 1 + tests/conftest.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/pyqasm/_logging.py create mode 100644 tests/conftest.py diff --git a/src/pyqasm/_logging.py b/src/pyqasm/_logging.py new file mode 100644 index 0000000..8c0f79e --- /dev/null +++ b/src/pyqasm/_logging.py @@ -0,0 +1,33 @@ +# # Copyright 2025 qBraid +# # +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at +# # +# # http://www.apache.org/licenses/LICENSE-2.0 +# # +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. + +""" +Module defining logging configuration for PyQASM. +This module sets up a logger for the PyQASM library, allowing for + +""" +import logging + +# Define a custom logger for the module +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s")) + +logger = logging.getLogger("pyqasm") +logger.addHandler(handler) +logger.setLevel(logging.ERROR) + +# disable propagation to avoid double logging +# messages to the root logger in case the root logging +# level changes +logger.propagate = False diff --git a/src/pyqasm/cli/validate.py b/src/pyqasm/cli/validate.py index 5bae1ec..c1ad09f 100644 --- a/src/pyqasm/cli/validate.py +++ b/src/pyqasm/cli/validate.py @@ -28,6 +28,7 @@ from pyqasm.exceptions import QasmParsingError, UnrollError, ValidationError logger = logging.getLogger(__name__) +logger.propagate = False def validate_paths_exist(paths: Optional[list[str]]) -> Optional[list[str]]: diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 5561f9a..462110d 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -17,7 +17,6 @@ """ -import logging import os import sys from typing import Optional, Type @@ -26,13 +25,7 @@ from openqasm3.parser import QASM3ParsingError from openqasm3.printer import dumps -# Define a custom logger for the module -handler = logging.StreamHandler() -handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s")) - -logger = logging.getLogger("pyqasm") -logger.addHandler(handler) -logger.setLevel(logging.WARNING) +from ._logging import logger class PyQasmError(Exception): diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index eeae214..9cef0b0 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -45,6 +45,7 @@ from pyqasm.validator import Qasm3Validator logger = logging.getLogger(__name__) +logger.propagate = False # pylint: disable-next=too-many-instance-attributes diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..897961a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for configuring pytest fixtures and logging settings for tests. +""" + +import pytest + +from src.pyqasm._logging import logger + + +# Automatically applied to all tests +@pytest.fixture(autouse=True) +def enable_logger_propagation(): + """Temporarily enable logger propagation for tests. This is because + caplog only captures logs from the root logger""" + original_propagate = logger.propagate + logger.propagate = True # Enable propagation for tests + yield + logger.propagate = original_propagate # Restore original behavior From 625a692fafb857d07de2ffe09613a56bc623f270 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 17 Apr 2025 15:11:52 +0530 Subject: [PATCH 13/16] fix header --- src/pyqasm/_logging.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pyqasm/_logging.py b/src/pyqasm/_logging.py index 8c0f79e..01310a9 100644 --- a/src/pyqasm/_logging.py +++ b/src/pyqasm/_logging.py @@ -1,16 +1,16 @@ -# # Copyright 2025 qBraid -# # -# # Licensed under the Apache License, Version 2.0 (the "License"); -# # you may not use this file except in compliance with the License. -# # You may obtain a copy of the License at -# # -# # http://www.apache.org/licenses/LICENSE-2.0 -# # -# # Unless required by applicable law or agreed to in writing, software -# # distributed under the License is distributed on an "AS IS" BASIS, -# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# # See the License for the specific language governing permissions and -# # limitations under the License. +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Module defining logging configuration for PyQASM. From c3829202e6fa878f693f60cd4f450b1233100d7b Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 18 Apr 2025 12:04:18 +0530 Subject: [PATCH 14/16] update declarations and assignments --- src/pyqasm/analyzer.py | 20 ++-- src/pyqasm/validator.py | 14 ++- src/pyqasm/visitor.py | 132 +++++++++++++++------ tests/qasm3/declarations/test_classical.py | 20 +++- tests/qasm3/declarations/test_quantum.py | 132 +++++++++++---------- tests/qasm3/resources/variables.py | 119 +++++++++++++++---- tests/qasm3/test_sizeof.py | 10 +- 7 files changed, 306 insertions(+), 141 deletions(-) diff --git a/src/pyqasm/analyzer.py b/src/pyqasm/analyzer.py index 9388d6e..648fd7a 100644 --- a/src/pyqasm/analyzer.py +++ b/src/pyqasm/analyzer.py @@ -85,25 +85,25 @@ def analyze_classical_indices( span=indices[0].span, ) - def _validate_index(index, dimension, var_name, span, dim_num): + def _validate_index(index, dimension, var_name, index_node, dim_num): if index < 0 or index >= dimension: raise_qasm3_error( message=f"Index {index} out of bounds for dimension {dim_num} " f"of variable '{var_name}'. Expected index in range [0, {dimension-1}]", err_type=ValidationError, - error_node=index, - span=span, + error_node=index_node, + span=index_node.span, ) - def _validate_step(start_id, end_id, step, span): + def _validate_step(start_id, end_id, step, index_node): if (step < 0 and start_id < end_id) or (step > 0 and start_id > end_id): direction = "less than" if step < 0 else "greater than" raise_qasm3_error( message=f"Index {start_id} is {direction} {end_id} but step" f" is {'negative' if step < 0 else 'positive'}", err_type=ValidationError, - error_node=indices, - span=span, + error_node=index_node, + span=index_node.span, ) for i, index in enumerate(indices): @@ -131,16 +131,16 @@ def _validate_step(start_id, end_id, step, span): if index.step is not None: step = expr_evaluator.evaluate_expression(index.step, reqd_type=IntType)[0] - _validate_index(start_id, var_dimensions[i], var.name, index.span, i) - _validate_index(end_id, var_dimensions[i], var.name, index.span, i) - _validate_step(start_id, end_id, step, index.span) + _validate_index(start_id, var_dimensions[i], var.name, index, i) + _validate_index(end_id, var_dimensions[i], var.name, index, i) + _validate_step(start_id, end_id, step, index) indices_list.append((start_id, end_id, step)) if isinstance(index, (Identifier, IntegerLiteral, Expression)): index_value = expr_evaluator.evaluate_expression(index, reqd_type=IntType)[0] curr_dimension = var_dimensions[i] # type: ignore[index] - _validate_index(index_value, curr_dimension, var.name, index.span, i) + _validate_index(index_value, curr_dimension, var.name, index, i) indices_list.append((index_value, index_value, 1)) diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index a990eb3..2db0cfb 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -194,24 +194,30 @@ def validate_variable_assignment_value( return type_casted_value @staticmethod - def validate_classical_type(base_type, base_size, var_name, span) -> None: + def validate_classical_type(base_type, base_size, var_name, op_node) -> None: """Validate the type and size of a classical variable. Args: base_type (Any): The base type of the variable. base_size (int): The size of the variable. var_name (str): The name of the variable. - span (Span): The span of the variable. + op_node (QASMNode): The operation node. Raises: ValidationError: If the type or size is invalid. """ if not isinstance(base_size, int) or base_size <= 0: - raise_qasm3_error(f"Invalid base size {base_size} for variable '{var_name}'", span=span) + raise_qasm3_error( + f"Invalid base size {base_size} for variable '{var_name}'", + error_node=op_node, + span=op_node.span, + ) if isinstance(base_type, FloatType) and base_size not in [32, 64]: raise_qasm3_error( - f"Invalid base size {base_size} for float variable '{var_name}'", span=span + f"Invalid base size {base_size} for float variable '{var_name}'", + error_node=op_node, + span=op_node.span, ) @staticmethod diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 9cef0b0..83efa0e 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -277,13 +277,22 @@ def _visit_quantum_register( logger.debug("Visiting register '%s'", str(register)) current_size = len(self._qubit_labels) - register_size = ( - 1 - if register.size is None - else Qasm3ExprEvaluator.evaluate_expression(register.size, const_expr=True)[ - 0 - ] # type: ignore[attr-defined] - ) + try: + register_size = ( + 1 + if register.size is None + else Qasm3ExprEvaluator.evaluate_expression(register.size, const_expr=True)[ + 0 + ] # type: ignore[attr-defined] + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid size '{dumps(register.size)}' for quantum " # type: ignore[arg-type] + f"register '{register.qubit.name}'", + error_node=register, + span=register.span, + raised_from=err, + ) register.size = qasm3_ast.IntegerLiteral(register_size) register_name = register.qubit.name # type: ignore[union-attr] @@ -1230,9 +1239,18 @@ def _visit_constant_declaration( error_node=statement, span=statement.span, ) - init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( - statement.init_expression, const_expr=True - ) + try: + init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( + statement.init_expression, const_expr=True + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid initialization value for constant '{var_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) + statements.extend(stmts) base_type = statement.type @@ -1242,14 +1260,20 @@ def _visit_constant_declaration( if base_type.size is None: base_size = 32 # default for now else: - base_size = Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[ - 0 - ] - if not isinstance(base_size, int) or base_size <= 0: + try: + base_size = Qasm3ExprEvaluator.evaluate_expression( + base_type.size, const_expr=True + )[0] + if not isinstance(base_size, int) or base_size <= 0: + raise ValidationError( + f"Invalid base size {base_size} for variable '{var_name}'" + ) + except ValidationError as err: raise_qasm3_error( - f"Invalid base size {base_size} for variable '{var_name}'", + f"Invalid base size for constant '{var_name}'", error_node=statement, span=statement.span, + raised_from=err, ) variable = Variable(var_name, base_type, base_size, [], init_value, is_constant=True) @@ -1312,12 +1336,20 @@ def _visit_classical_declaration( base_size = 1 if not isinstance(base_type, qasm3_ast.BoolType): initial_size = 1 if isinstance(base_type, qasm3_ast.BitType) else 32 - base_size = ( - initial_size - if not hasattr(base_type, "size") or base_type.size is None - else Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[0] - ) - Qasm3Validator.validate_classical_type(base_type, base_size, var_name, statement.span) + try: + base_size = ( + initial_size + if not hasattr(base_type, "size") or base_type.size is None + else Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[0] + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid base size for variable '{var_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) + Qasm3Validator.validate_classical_type(base_type, base_size, var_name, statement) # initialize the bit register if isinstance(base_type, qasm3_ast.BitType): @@ -1364,10 +1396,18 @@ def _visit_classical_declaration( qasm3_ast.QuantumMeasurementStatement(measurement, statement.identifier) ) # type: ignore else: - init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( - statement.init_expression - ) - statements.extend(stmts) + try: + init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( + statement.init_expression + ) + statements.extend(stmts) + except ValidationError as err: + raise_qasm3_error( + f"Invalid initialization value for variable '{var_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) variable = Variable( var_name, @@ -1382,13 +1422,32 @@ def _visit_classical_declaration( if statement.init_expression: if isinstance(init_value, np.ndarray): assert variable.dims is not None - Qasm3Validator.validate_array_assignment_values(variable, variable.dims, init_value) + try: + Qasm3Validator.validate_array_assignment_values( + variable, variable.dims, init_value + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid initialization value for array '{var_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) else: - variable.value = Qasm3Validator.validate_variable_assignment_value( - variable, init_value, op_node=statement - ) + try: + variable.value = Qasm3Validator.validate_variable_assignment_value( + variable, init_value, op_node=statement + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid initialization value for variable '{var_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) self._add_var_in_scope(variable) + # special handling for bit[...] if isinstance(base_type, qasm3_ast.BitType): self._global_creg_size_map[var_name] = base_size current_classical_size = len(self._clbit_labels) @@ -1492,10 +1551,17 @@ def _visit_classical_assignment( l_indices = lvalue.indices[0] else: l_indices = [idx[0] for idx in lvalue.indices] # type: ignore[assignment, index] - - validated_l_indices = Qasm3Analyzer.analyze_classical_indices( - l_indices, lvar, Qasm3ExprEvaluator # type: ignore[arg-type] - ) + try: + validated_l_indices = Qasm3Analyzer.analyze_classical_indices( + l_indices, lvar, Qasm3ExprEvaluator # type: ignore[arg-type] + ) + except ValidationError as err: + raise_qasm3_error( + f"Invalid index for variable '{lvar_name}'", + error_node=statement, + span=statement.span, + raised_from=err, + ) Qasm3Transformer.update_array_element( multi_dim_arr=lvar.value, # type: ignore[union-attr, arg-type] indices=validated_l_indices, diff --git a/tests/qasm3/declarations/test_classical.py b/tests/qasm3/declarations/test_classical.py index 56f423e..b600f6a 100644 --- a/tests/qasm3/declarations/test_classical.py +++ b/tests/qasm3/declarations/test_classical.py @@ -370,14 +370,22 @@ def test_array_range_assignment(): @pytest.mark.parametrize("test_name", DECLARATION_TESTS.keys()) -def test_incorrect_declarations(test_name): - qasm_input, error_message = DECLARATION_TESTS[test_name] +def test_incorrect_declarations(test_name, caplog): + qasm_input, error_message, line_num, col_num, err_line = DECLARATION_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - loads(qasm_input).validate() + with caplog.at_level("ERROR"): + loads(qasm_input).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text @pytest.mark.parametrize("test_name", ASSIGNMENT_TESTS.keys()) -def test_incorrect_assignments(test_name): - qasm_input, error_message = ASSIGNMENT_TESTS[test_name] +def test_incorrect_assignments(test_name, caplog): + qasm_input, error_message, line_num, col_num, err_line = ASSIGNMENT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - loads(qasm_input).validate() + with caplog.at_level("ERROR"): + loads(qasm_input).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text diff --git a/tests/qasm3/declarations/test_quantum.py b/tests/qasm3/declarations/test_quantum.py index 2e053b2..3f5fd63 100644 --- a/tests/qasm3/declarations/test_quantum.py +++ b/tests/qasm3/declarations/test_quantum.py @@ -125,63 +125,75 @@ def test_qubit_clbit_declarations(): check_unrolled_qasm(unrolled_qasm, expected_qasm) -def test_qubit_redeclaration_error(): - """Test redeclaration of qubit""" - with pytest.raises(ValidationError, match="Re-declaration of quantum register with name 'q1'"): - qasm3_string = """ - OPENQASM 3.0; - include "stdgates.inc"; - qubit q1; - qubit q1; - """ - loads(qasm3_string).validate() - - -def test_invalid_qubit_name(): - """Test that qubit name can not be one of constants""" - with pytest.raises( - ValidationError, match="Can not declare quantum register with keyword name 'pi'" - ): - qasm3_string = """ - OPENQASM 3.0; - include "stdgates.inc"; - qubit pi; - """ - loads(qasm3_string).validate() - - -def test_clbit_redeclaration_error(): - """Test redeclaration of clbit""" - with pytest.raises(ValidationError, match=r"Re-declaration of variable 'c1'"): - qasm3_string = """ - OPENQASM 3.0; - include "stdgates.inc"; - bit c1; - bit[4] c1; - """ - loads(qasm3_string).validate() - - -def test_non_constant_size(): - """Test non-constant size in qubit and clbit declarations""" - with pytest.raises( - ValidationError, match=r"Expected variable 'N' to be constant in given expression" - ): - qasm3_string = """ - OPENQASM 3.0; - include "stdgates.inc"; - int[32] N = 10; - qubit[N] q; - """ - loads(qasm3_string).validate() - - with pytest.raises( - ValidationError, match=r"Expected variable 'size' to be constant in given expression" - ): - qasm3_string = """ - OPENQASM 3.0; - include "stdgates.inc"; - int[32] size = 10; - bit[size] c; - """ - loads(qasm3_string).validate() +@pytest.mark.parametrize( + "qasm_code,error_message,line_num,col_num,err_line", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit q1; + qubit q1; + """, + "Re-declaration of quantum register with name 'q1'", + 5, + 12, + "qubit[1] q1;", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit pi; + """, + "Can not declare quantum register with keyword name 'pi'", + 4, + 12, + "qubit[1] pi;", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + bit c1; + bit[4] c1; + """, + r"Re-declaration of variable 'c1'", + 5, + 12, + "bit[4] c1;", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int[32] N = 10; + qubit[N] q; + """, + r"Invalid size 'N' for quantum register 'q'", + 5, + 12, + "qubit[N] q;", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int[32] size = 10; + bit[size] c; + """, + r"Invalid base size for variable 'c'", + 5, + 15, + "bit[size] c;", + ), + ], +) +def test_quantum_declarations_errors(qasm_code, error_message, line_num, col_num, err_line, caplog): + """Test various error cases with qubit and bit declarations""" + with pytest.raises(ValidationError, match=error_message): + with caplog.at_level("ERROR"): + loads(qasm_code).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index 03b51d4..514bff6 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -16,7 +16,6 @@ Module defining QASM3 incorrect variable tests. """ - DECLARATION_TESTS = { "keyword_redeclaration": ( """ @@ -25,6 +24,9 @@ int pi; """, "Can not declare variable with keyword name pi", + 4, # Line number + 8, # Column number + "int pi;", # Complete line ), "const_keyword_redeclaration": ( """ @@ -33,6 +35,9 @@ const int pi = 3; """, "Can not declare variable with keyword name pi", + 4, + 8, + "const int pi = 3;", ), "variable_redeclaration": ( """ @@ -43,6 +48,9 @@ uint x; """, "Re-declaration of variable 'x'", + 6, + 8, + "uint x;", ), "variable_redeclaration_with_qubits_1": ( """ @@ -52,6 +60,9 @@ qubit x; """, "Re-declaration of quantum register with name 'x'", + 5, + 8, + "qubit[1] x;", ), "variable_redeclaration_with_qubits_2": ( """ @@ -61,6 +72,9 @@ int x; """, "Re-declaration of variable 'x'", + 5, + 8, + "int x;", ), "const_variable_redeclaration": ( """ @@ -70,6 +84,9 @@ const float x = 3.4; """, "Re-declaration of variable 'x'", + 5, + 8, + "const float x = 3.4;", ), "invalid_int_size": ( """ @@ -78,6 +95,9 @@ int[32.1] x; """, "Invalid base size 32.1 for variable 'x'", + 4, + 8, + "int[32.1] x;", ), "invalid_const_int_size": ( """ @@ -85,7 +105,10 @@ include "stdgates.inc"; const int[32.1] x = 3; """, - "Invalid base size 32.1 for variable 'x'", + "Invalid base size for constant 'x'", + 4, + 8, + "const int[32.1] x = 3;", ), "const_declaration_with_non_const": ( """ @@ -94,7 +117,10 @@ int[32] x = 5; const int[32] y = x + 5; """, - "Expected variable 'x' to be constant in given expression", + "Invalid initialization value for constant 'y'", + 5, + 8, + "const int[32] y = x + 5;", ), "const_declaration_with_non_const_size": ( """ @@ -103,7 +129,10 @@ int[32] x = 5; const int[x] y = 5; """, - "Expected variable 'x' to be constant in given expression", + "Invalid base size for constant 'y'", + 5, + 8, + "const int[x] y = 5;", ), "invalid_float_size": ( """ @@ -113,6 +142,9 @@ float[23] x; """, "Invalid base size 23 for float variable 'x'", + 5, + 8, + "float[23] x;", ), "unsupported_types": ( """ @@ -121,7 +153,10 @@ angle x = 3.4; """, - "Invalid type '' for variable 'x'", + "Invalid initialization value for variable 'x'", + 5, + 8, + "angle x = 3.4;", ), "imaginary_variable": ( """ @@ -130,7 +165,10 @@ int x = 1 + 3im; """, - "Unsupported expression type ''", + "Invalid initialization value for variable 'x'", + 5, + 8, + "int x = 1 + 3.0im;", ), "invalid_array_dimensions": ( """ @@ -140,6 +178,9 @@ array[int[32], 1, 2.1] x; """, "Invalid dimension size 2.1 in array declaration for 'x'", + 5, + 8, + "array[int[32], 1, 2.1] x;", ), "extra_array_dimensions": ( """ @@ -149,6 +190,9 @@ array[int[32], 1, 2, 3, 4, 5, 6, 7, 8] x; """, "Invalid dimensions 8 for array declaration for 'x'. Max allowed dimensions is 7", + 5, + 8, + "array[int[32], 1, 2, 3, 4, 5, 6, 7, 8] x;", ), "dimension_mismatch_1": ( """ @@ -157,7 +201,10 @@ array[int[32], 1, 2] x = {1,2,3}; """, - "Invalid dimensions for array assignment to variable 'x'. Expected 1 but got 3", + "Invalid initialization value for array 'x'", + 5, + 8, + "array[int[32], 1, 2] x = {1, 2, 3};", ), "dimension_mismatch_2": ( """ @@ -166,7 +213,10 @@ array[int[32], 3, 1, 2] x = {1,2,3}; """, - "Invalid dimensions for array assignment to variable x. Expected 3 but got 1", + "Invalid initialization value for array 'x'", + 5, + 8, + "array[int[32], 3, 1, 2] x = {1, 2, 3};", ), "invalid_bit_type_array_1": ( """ @@ -176,6 +226,9 @@ array[bit, 3] x; """, "Can not declare array x with type 'bit'", + 5, + 8, + "array[bit, 3] x;", ), "invalid_bit_type_array_2": ( """ @@ -185,9 +238,11 @@ array[bit[32], 3] x; """, "Can not declare array x with type 'bit'", + 5, + 8, + "array[bit[32], 3] x;", ), } - ASSIGNMENT_TESTS = { "undefined_variable_assignment": ( """ @@ -200,6 +255,9 @@ """, "Undefined variable x in assignment", + 7, # Line number + 8, # Column number + "x = 3;", # Complete line ), "assignment_to_constant": ( """ @@ -210,6 +268,9 @@ x = 4; """, "Assignment to constant variable x not allowed", + 6, + 8, + "x = 4;", ), "invalid_assignment_type": ( """ @@ -218,11 +279,10 @@ bit x = 3.3; """, - ( - "Cannot cast to . " - "Invalid assignment of type to variable x of type " - "" - ), + "Invalid initialization value for variable 'x'", + 5, + 8, + "bit x = 3.3;", ), "int_out_of_range": ( """ @@ -231,7 +291,10 @@ int[32] x = 1<<64; """, - f"Value {2**64} out of limits for variable 'x' with base size 32", + f"Invalid initialization value for variable 'x'", + 5, + 8, + "int[32] x = 1 << 64;", ), "float32_out_of_range": ( """ @@ -240,7 +303,10 @@ float[32] x = 123456789123456789123456789123456789123456789.1; """, - "Value .* out of limits for variable 'x' with base size 32", + "Invalid initialization value for variable 'x'", + 5, + 8, + "float[32] x = 1.2345678912345679e+44;", ), "indexing_non_array": ( """ @@ -250,7 +316,10 @@ int x = 3; x[0] = 4; """, - "Indexing error. Variable x is not an array", + "Invalid index for variable 'x'", + 6, + 8, + "x[0] = 4;", ), "incorrect_num_dims": ( """ @@ -260,7 +329,10 @@ array[int[32], 1, 2, 3] x; x[0] = 3; """, - "Invalid number of indices for variable x. Expected 3 but got 1", + "Invalid index for variable 'x'", + 6, + 8, + "x[0] = 3;", ), "non_nnint_index": ( """ @@ -270,8 +342,10 @@ array[int[32], 3] x; x[0.1] = 3; """, - "Invalid value 0.1 with type for " - "required type ", + "Invalid index for variable 'x'", + 6, + 8, + "x[0.1] = 3;", ), "index_out_of_range": ( """ @@ -281,6 +355,9 @@ array[int[32], 3] x; x[3] = 3; """, - "Index 3 out of bounds for dimension 0 of variable 'x'", + "Invalid index for variable 'x'", + 6, + 8, + "x[3] = 3;", ), } diff --git a/tests/qasm3/test_sizeof.py b/tests/qasm3/test_sizeof.py index 7a501bd..9ab82a6 100644 --- a/tests/qasm3/test_sizeof.py +++ b/tests/qasm3/test_sizeof.py @@ -81,7 +81,7 @@ def test_sizeof_multiple_types(): def test_unsupported_target(caplog): """Test sizeof over index expressions""" - with pytest.raises(ValidationError, match=r"Unsupported target type .*"): + with pytest.raises(ValidationError, match=r"Invalid initialization value for variable 'size1'"): with caplog.at_level("ERROR"): qasm3_string = """ OPENQASM 3; @@ -98,9 +98,7 @@ def test_unsupported_target(caplog): def test_sizeof_on_non_array(caplog): """Test sizeof on a non-array""" - with pytest.raises( - ValidationError, match="Invalid sizeof usage, variable 'my_int' is not an array." - ): + with pytest.raises(ValidationError, match="Invalid initialization value for variable 'size1'"): with caplog.at_level("ERROR"): qasm3_string = """ OPENQASM 3; @@ -117,9 +115,7 @@ def test_sizeof_on_non_array(caplog): def test_out_of_bounds_reference(caplog): """Test sizeof on an out of bounds reference""" - with pytest.raises( - ValidationError, match="Index 3 out of bounds for array 'my_ints' with 2 dimensions" - ): + with pytest.raises(ValidationError, match="Invalid initialization value for variable 'size1'"): with caplog.at_level("ERROR"): qasm3_string = """ OPENQASM 3; From 776a6fe91e5f34efce990217d6d216f1f59032fc Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 18 Apr 2025 15:51:36 +0530 Subject: [PATCH 15/16] fix subroutine tests and complete --- src/pyqasm/exceptions.py | 4 +- src/pyqasm/subroutines.py | 150 +++++++++++------- src/pyqasm/validator.py | 2 +- src/pyqasm/visitor.py | 2 +- tests/qasm3/declarations/test_quantum.py | 1 + tests/qasm3/resources/subroutines.py | 106 +++++++++++-- tests/qasm3/resources/variables.py | 2 +- tests/qasm3/subroutines/test_subroutines.py | 36 +++-- .../subroutines/test_subroutines_arrays.py | 12 +- 9 files changed, 225 insertions(+), 90 deletions(-) diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py index 462110d..7a465c0 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -81,10 +81,10 @@ def raise_qasm3_error( error_parts.append("\n >>>>>> " + dumps(error_node, indent=" ") + "\n") elif isinstance(error_node, list): error_parts.append( - "\n >>>>>> " - + " , ".join(dumps(node, indent=" ") for node in error_node + "\n") + "\n >>>>>> " + " , ".join(dumps(node, indent=" ") for node in error_node) ) except Exception as _: # pylint: disable = broad-exception-caught + print(_) error_parts.append("\n >>>>>> " + str(error_node)) if error_parts: diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index d433838..4f4096e 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -24,13 +24,14 @@ Identifier, IndexExpression, IntType, + QASMNode, QubitDeclaration, ) from openqasm3.printer import dumps from pyqasm.analyzer import Qasm3Analyzer from pyqasm.elements import Variable -from pyqasm.exceptions import raise_qasm3_error +from pyqasm.exceptions import ValidationError, raise_qasm3_error from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.transformer import Qasm3Transformer from pyqasm.validator import Qasm3Validator @@ -74,7 +75,7 @@ def get_fn_actual_arg_name(actual_arg: Identifier | IndexExpression) -> Optional return actual_arg_name @classmethod - def process_classical_arg(cls, formal_arg, actual_arg, fn_name, span): + def process_classical_arg(cls, formal_arg, actual_arg, fn_name, fn_call): """Process the classical argument for a function call. Args: @@ -90,15 +91,15 @@ def process_classical_arg(cls, formal_arg, actual_arg, fn_name, span): if isinstance(formal_arg.type, ArrayReferenceType): return cls._process_classical_arg_by_reference( - formal_arg, actual_arg, actual_arg_name, fn_name, span + formal_arg, actual_arg, actual_arg_name, fn_name, fn_call ) return cls._process_classical_arg_by_value( - formal_arg, actual_arg, actual_arg_name, fn_name, span + formal_arg, actual_arg, actual_arg_name, fn_name, fn_call ) @classmethod # pylint: disable-next=too-many-arguments def _process_classical_arg_by_value( - cls, formal_arg, actual_arg, actual_arg_name, fn_name, span + cls, formal_arg, actual_arg, actual_arg_name, fn_name, fn_call ): """ Process the classical argument for a function call. @@ -120,14 +121,19 @@ def _process_classical_arg_by_value( # in the scope of the function fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + formal_args_desc = " , ".join(dumps(arg, indent=" ") for arg in fn_defn.arguments) if actual_arg_name: # actual arg is a variable not literal if actual_arg_name in cls.visitor_obj._global_qreg_size_map: + formal_args_desc = " , ".join( + dumps(arg, indent=" ") for arg in fn_defn.arguments + ) raise_qasm3_error( f"Expecting classical argument for '{formal_arg.name.name}'. " - f"Qubit register '{actual_arg_name}' found for function '{fn_name}'", - error_node=fn_defn.arguments, - span=span, + f"Qubit register '{actual_arg_name}' found for function '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) # 2. as we have pushed the scope for fn, we need to check in parent @@ -135,9 +141,10 @@ def _process_classical_arg_by_value( if not cls.visitor_obj._check_in_scope(actual_arg_name): raise_qasm3_error( f"Undefined variable '{actual_arg_name}' used" - f" for function call '{fn_name}'", - error_node=actual_arg, - span=span, + f" for function call '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0] @@ -153,7 +160,7 @@ def _process_classical_arg_by_value( @classmethod # pylint: disable-next=too-many-arguments,too-many-locals,too-many-branches def _process_classical_arg_by_reference( - cls, formal_arg, actual_arg, actual_arg_name, fn_name, span + cls, formal_arg, actual_arg, actual_arg_name, fn_name, fn_call ): """Process the classical args by reference in the QASM3 visitor. Currently being used for array references only. @@ -177,6 +184,8 @@ def _process_classical_arg_by_reference( formal_arg_base_size = Qasm3ExprEvaluator.evaluate_expression( formal_arg.type.base_type.size )[0] + fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + array_expected_type_msg = ( "Expecting type 'array[" f"{formal_arg.type.base_type.__class__.__name__.lower().removesuffix('type')}" @@ -184,31 +193,34 @@ def _process_classical_arg_by_reference( f" in function '{fn_name}'. " ) - fn_defn = cls.visitor_obj._subroutine_defns.get(fn_name) + formal_args_desc = " , ".join(dumps(arg, indent=" ") for arg in fn_defn.arguments) if actual_arg_name is None: raise_qasm3_error( array_expected_type_msg + f"Literal '{Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]}' " - + "found in function call", - error_node=actual_arg, - span=span, + + "found in function call\n" + + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) if actual_arg_name in cls.visitor_obj._global_qreg_size_map: raise_qasm3_error( array_expected_type_msg - + f"Qubit register '{actual_arg_name}' found for function call", - error_node=actual_arg, - span=span, + + f"Qubit register '{actual_arg_name}' found for function call\n" + + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) # verify actual argument is defined in the parent scope of function call if not cls.visitor_obj._check_in_scope(actual_arg_name): raise_qasm3_error( - f"Undefined variable '{actual_arg_name}' used for function call '{fn_name}'", - error_node=actual_arg, - span=span, + f"Undefined variable '{actual_arg_name}' used for function call '{fn_name}'\n" + + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) array_reference = cls.visitor_obj._get_from_visible_scope(actual_arg_name) @@ -218,9 +230,10 @@ def _process_classical_arg_by_reference( if not array_reference.dims: raise_qasm3_error( array_expected_type_msg - + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", - error_node=fn_defn.arguments, - span=span, + + f"Variable '{actual_arg_name}' has type '{actual_type_string}'\n" + + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) # The base types of the elements in array should match @@ -230,9 +243,10 @@ def _process_classical_arg_by_reference( if formal_arg.type.base_type != actual_arg_type or formal_arg_base_size != actual_arg_size: raise_qasm3_error( array_expected_type_msg - + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", - error_node=fn_defn.arguments, - span=span, + + f"Variable '{actual_arg_name}' has type '{actual_type_string}'\n" + + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) # The dimensions passed in the formal arg should be @@ -243,28 +257,35 @@ def _process_classical_arg_by_reference( formal_dimensions_raw = formal_arg.type.dimensions # 1. Either we will have #dim = <> if not isinstance(formal_dimensions_raw, list): - num_formal_dimensions = Qasm3ExprEvaluator.evaluate_expression( - formal_dimensions_raw, reqd_type=IntType, const_expr=True - )[0] + try: + num_formal_dimensions = Qasm3ExprEvaluator.evaluate_expression( + formal_dimensions_raw, reqd_type=IntType, const_expr=True + )[0] + except ValidationError as err: + raise_qasm3_error( + f"Invalid dimension size {dumps(formal_dimensions_raw)} " + f"for '{formal_arg.name.name}' in function '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, + raised_from=err, + ) # 2. or we will have a list of the dimensions in the formal arg else: num_formal_dimensions = len(formal_dimensions_raw) - if num_formal_dimensions <= 0: - raise_qasm3_error( + if num_formal_dimensions <= 0 or num_formal_dimensions > len(actual_dimensions): + error_msg = ( f"Invalid number of dimensions {num_formal_dimensions}" - f" for '{formal_arg.name.name}' in function '{fn_name}'", - error_node=fn_defn.arguments, - span=span, + if num_formal_dimensions <= 0 + else f"Dimension mismatch. Expected {num_formal_dimensions} dimensions but " + f"variable '{actual_arg_name}' has {len(actual_dimensions)}" ) - - if num_formal_dimensions > len(actual_dimensions): raise_qasm3_error( - f"Dimension mismatch for '{formal_arg.name.name}' in function '{fn_name}'. " - f"Expected {num_formal_dimensions} dimensions but" - f" variable '{actual_arg_name}' has {len(actual_dimensions)}", - error_node=fn_defn.arguments, - span=span, + f"{error_msg} for '{formal_arg.name.name}' in function '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) formal_dimensions = [] @@ -276,25 +297,36 @@ def _process_classical_arg_by_reference( for idx, (formal_dim, actual_dim) in enumerate( zip(formal_dimensions_raw, actual_dimensions) ): - formal_dim = Qasm3ExprEvaluator.evaluate_expression( - formal_dim, reqd_type=IntType, const_expr=True - )[0] - if formal_dim <= 0: - raise_qasm3_error( - f"Invalid dimension size {formal_dim} for '{formal_arg.name.name}'" - f" in function '{fn_name}'", - error_node=fn_defn.arguments, - span=span, + try: + formal_dim = Qasm3ExprEvaluator.evaluate_expression( + formal_dim, reqd_type=IntType, const_expr=True + )[0] + if formal_dim <= 0 or formal_dim > actual_dim: + error_msg = ( + f"Invalid dimension size {formal_dim}" + if formal_dim <= 0 + else f"Dimension mismatch. Expected dimension {idx} with " + f"size >= {formal_dim} but got {actual_dim}" + ) + raise_qasm3_error( + f"{error_msg} for '{formal_arg.name.name}' in function '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, + ) + formal_dimensions.append(formal_dim) + except ValidationError as err: + formal_arg_str = ( + dumps(formal_dim) if isinstance(formal_dim, QASMNode) else formal_dim ) - if actual_dim < formal_dim: raise_qasm3_error( - f"Dimension mismatch for '{formal_arg.name.name}'" - f" in function '{fn_name}'. Expected dimension {idx} with size" - f" >= {formal_dim} but got {actual_dim}", - error_node=fn_defn.arguments, - span=span, + f"Invalid dimension size {formal_arg_str}" + f" for '{formal_arg.name.name}' in function '{fn_name}'\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, + raised_from=err, ) - formal_dimensions.append(formal_dim) readonly_arr = formal_arg.access == AccessControl.readonly actual_array_view = array_reference.value diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index 2db0cfb..90d0ac4 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -320,7 +320,7 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements if return_value is None: raise_qasm3_error( f"Return type mismatch for subroutine '{subroutine_def.name.name}'." - f" Expected {subroutine_def.return_type} but got void", + f" Expected {type(subroutine_def.return_type)} but got void", error_node=return_statement, span=return_statement.span, ) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 83efa0e..41fee12 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1865,7 +1865,7 @@ def _visit_function_call( if isinstance(formal_arg, qasm3_ast.ClassicalArgument): classical_vars.append( Qasm3SubroutineProcessor.process_classical_arg( - formal_arg, actual_arg, fn_name, statement.span + formal_arg, actual_arg, fn_name, statement ) ) else: diff --git a/tests/qasm3/declarations/test_quantum.py b/tests/qasm3/declarations/test_quantum.py index 3f5fd63..39c38a8 100644 --- a/tests/qasm3/declarations/test_quantum.py +++ b/tests/qasm3/declarations/test_quantum.py @@ -189,6 +189,7 @@ def test_qubit_clbit_declarations(): ), ], ) +# pylint: disable-next=too-many-arguments def test_quantum_declarations_errors(qasm_code, error_message, line_num, col_num, err_line, caplog): """Test various error cases with qubit and bit declarations""" with pytest.raises(ValidationError, match=error_message): diff --git a/tests/qasm3/resources/subroutines.py b/tests/qasm3/resources/subroutines.py index 7d8039b..5d3dd8e 100644 --- a/tests/qasm3/resources/subroutines.py +++ b/tests/qasm3/resources/subroutines.py @@ -16,7 +16,6 @@ Module defining subroutine tests. """ - SUBROUTINE_INCORRECT_TESTS = { "undeclared_call": ( """ @@ -26,6 +25,9 @@ my_function(1); """, "Undefined subroutine 'my_function' was called", + 5, # Line number + 8, # Column number + "my_function(1)", # Complete line ), "redefinition_raises_error": ( """ @@ -43,6 +45,9 @@ def my_function(qubit q) -> float[32] { qubit q; """, "Redefinition of subroutine 'my_function'", + 9, + 8, + "def my_function(qubit q) -> float[32]", ), "redefinition_raises_error_2": ( """ @@ -56,6 +61,9 @@ def my_function(qubit q) { my_function(q); """, "Re-declaration of variable 'q'", + 5, + 12, + "int[32] q = 1;", ), "incorrect_param_count_1": ( """ @@ -70,6 +78,9 @@ def my_function(qubit q, qubit r) { my_function(q); """, "Parameter count mismatch for subroutine 'my_function'. Expected 2 but got 1 in call", + 10, + 8, + "my_function(q)", ), "incorrect_param_count_2": ( """ @@ -84,6 +95,9 @@ def my_function(int[32] q) { my_function(q, q); """, "Parameter count mismatch for subroutine 'my_function'. Expected 1 but got 2 in call", + 10, + 8, + "my_function(q, q)", ), "return_value_mismatch": ( """ @@ -99,6 +113,9 @@ def my_function(qubit q) { my_function(q); """, "Return type mismatch for subroutine 'my_function'.", + 8, + 12, + "return a;", ), "return_value_mismatch_2": ( """ @@ -114,6 +131,9 @@ def my_function(qubit q) -> int[32] { my_function(q); """, "Return type mismatch for subroutine 'my_function'.", + 8, + 12, + "return;", ), "subroutine_keyword_naming": ( """ @@ -128,6 +148,9 @@ def pi(qubit q) { pi(q); """, "Subroutine name 'pi' is a reserved keyword", + 5, + 8, + "def pi(qubit q) {", ), "qubit_size_arg_mismatch": ( """ @@ -142,6 +165,9 @@ def my_function(qubit[3] q) { my_function(q); """, "Qubit register size mismatch for function 'my_function'.", + 10, + 8, + "my_function(q)", ), "subroutine_var_name_conflict": ( """ @@ -156,6 +182,9 @@ def a(qubit q) { a(q); """, r"Can not declare subroutine with name 'a' .*", + 5, + 8, + "def a(qubit q) {", ), "undeclared_register_usage": ( """ @@ -171,6 +200,9 @@ def my_function(qubit q) { my_function(b); """, "Expecting qubit argument for 'q'. Qubit register 'b' not found for function 'my_function'", + 11, + 8, + "my_function(b)", ), "test_invalid_qubit_size": ( """ @@ -185,6 +217,9 @@ def my_function(qubit[-3] q) { my_function(q); """, "Invalid qubit size '-3' for variable 'q' in function 'my_function'", + 5, + 24, + "qubit[-3] q", ), "test_type_mismatch_for_function": ( """ @@ -200,6 +235,9 @@ def my_function(int[32] a, qubit q) { my_function(q, b); """, "Expecting classical argument for 'a'. Qubit register 'q' found for function 'my_function'", + 11, + 8, + "my_function(q, b)", ), "test_duplicate_qubit_args": ( """ @@ -214,6 +252,9 @@ def my_function(qubit[3] p, qubit[1] q) { my_function(q[0:3], q[2]); """, r"Duplicate qubit argument for register 'q' in function call for 'my_function'", + 10, + 8, + "my_function(q[0:3], q[2])", ), "undefined_variable_in_actual_arg_1": ( """ @@ -228,6 +269,9 @@ def my_function(int [32] a) { my_function(b); """, "Undefined variable 'b' used for function call 'my_function'", + 10, + 8, + "my_function(b)", ), "undefined_array_arg_in_function_call": ( """ @@ -240,6 +284,9 @@ def my_function(readonly array[int[32], 1, 2] a) { my_function(b); """, "Undefined variable 'b' used for function call 'my_function'", + 8, + 8, + "my_function(b)", ), } @@ -257,7 +304,10 @@ def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { my_function(q, arr); """, r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." - r" Variable 'arr' has type 'int\[8\]'.", + r" Variable 'arr' has type 'int\[8\]'", + 10, + 8, + "my_function(q, arr)", ), "literal_raises_error": ( """ @@ -272,6 +322,9 @@ def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { """, r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." r" Literal '5' found in function call", + 9, + 8, + "my_function(q, 5)", ), "type_mismatch_in_array": ( """ @@ -286,7 +339,10 @@ def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { my_function(q, arr); """, r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." - r" Variable 'arr' has type 'array\[uint\[32\], 2, 2\]'.", + r" Variable 'arr' has type 'array\[uint\[32\], 2, 2\]'", + 10, + 8, + "my_function(q, arr)", ), "dimension_count_mismatch_1": ( """ @@ -300,8 +356,11 @@ def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { array[int[8], 2] arr; my_function(q, arr); """, - r"Dimension mismatch for 'my_arr' in function 'my_function'. Expected 2 dimensions" - r" but variable 'arr' has 1", + r"Dimension mismatch. Expected 2 dimensions but variable 'arr'" + r" has 1 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "dimension_count_mismatch_2": ( """ @@ -315,8 +374,11 @@ def my_function(qubit a, readonly array[int[8], #dim = 4] my_arr) { array[int[8], 2, 2] arr; my_function(q, arr); """, - r"Dimension mismatch for 'my_arr' in function 'my_function'. Expected 4 dimensions " - r"but variable 'arr' has 2", + r"Dimension mismatch. Expected 4 dimensions but variable 'arr'" + r" has 2 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "qubit_passed_as_array": ( """ @@ -331,6 +393,9 @@ def my_function(mutable array[int[8], 2, 2] my_arr) { """, r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." r" Qubit register 'q' found for function call", + 9, + 8, + "my_function(q)", ), "invalid_dimension_number": ( """ @@ -345,6 +410,9 @@ def my_function(qubit a, readonly array[int[8], #dim = -3] my_arr) { my_function(q, arr); """, r"Invalid number of dimensions -3 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "invalid_non_int_dimensions_1": ( """ @@ -358,8 +426,10 @@ def my_function(qubit a, mutable array[int[8], #dim = 2.5] my_arr) { array[int[8], 2, 2] arr; my_function(q, arr); """, - r"Invalid value 2.5 with type for required type " - r"", + r"Invalid dimension size 2.5 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "invalid_non_int_dimensions_2": ( """ @@ -373,8 +443,10 @@ def my_function(qubit a, readonly array[int[8], 2.5, 2] my_arr) { array[int[8], 2, 2] arr; my_function(q, arr); """, - r"Invalid value 2.5 with type for required type" - r" ", + r"Invalid dimension size 2.5 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "extra_dimensions_for_array": ( """ @@ -388,8 +460,10 @@ def my_function(qubit a, mutable array[int[8], 4, 2] my_arr) { array[int[8], 2, 2] arr; my_function(q, arr); """, - r"Dimension mismatch for 'my_arr' in function 'my_function'. " - r"Expected dimension 0 with size >= 4 but got 2", + r"Invalid dimension size 4 for 'my_arr' in function 'my_function'", + 10, + 8, + "my_function(q, arr)", ), "invalid_array_dimensions_formal_arg": ( """ @@ -403,6 +477,9 @@ def my_function(readonly array[int[32], -1, 2] a) { my_function(b); """, r"Invalid dimension size -1 for 'a' in function 'my_function'", + 9, + 8, + "my_function(b)", ), "invalid_array_mutation_for_readonly_arg": ( """ @@ -417,5 +494,8 @@ def my_function(readonly array[int[32], 1, 2] a) { my_function(b); """, r"Assignment to readonly variable 'a' not allowed in function call", + 6, + 12, + "a[1][0] = 5", ), } diff --git a/tests/qasm3/resources/variables.py b/tests/qasm3/resources/variables.py index 514bff6..2f1c749 100644 --- a/tests/qasm3/resources/variables.py +++ b/tests/qasm3/resources/variables.py @@ -291,7 +291,7 @@ int[32] x = 1<<64; """, - f"Invalid initialization value for variable 'x'", + "Invalid initialization value for variable 'x'", 5, 8, "int[32] x = 1 << 64;", diff --git a/tests/qasm3/subroutines/test_subroutines.py b/tests/qasm3/subroutines/test_subroutines.py index fe5399e..bfbaec9 100644 --- a/tests/qasm3/subroutines/test_subroutines.py +++ b/tests/qasm3/subroutines/test_subroutines.py @@ -302,7 +302,7 @@ def my_function_2(qubit[2] q2) { @pytest.mark.parametrize("data_type", ["int[32] a = 1;", "float[32] a = 1.0;", "bit a = 0;"]) -def test_return_value_mismatch(data_type): +def test_return_value_mismatch(data_type, caplog): """Test that returning a value of incorrect type raises error.""" qasm_str = ( """OPENQASM 3.0; @@ -323,11 +323,15 @@ def my_function(qubit q) { with pytest.raises( ValidationError, match=r"Return type mismatch for subroutine 'my_function'.*" ): - loads(qasm_str).validate() + with caplog.at_level("ERROR"): + loads(qasm_str).validate() + + assert "Error at line 7, column 8" in caplog.text + assert "return a" in caplog.text @pytest.mark.parametrize("keyword", ["pi", "euler", "tau"]) -def test_subroutine_keyword_naming(keyword): +def test_subroutine_keyword_naming(keyword, caplog): """Test that using a keyword as a subroutine name raises error.""" qasm_str = f"""OPENQASM 3.0; include "stdgates.inc"; @@ -341,11 +345,15 @@ def {keyword}(qubit q) {{ """ with pytest.raises(ValidationError, match=f"Subroutine name '{keyword}' is a reserved keyword"): - loads(qasm_str).validate() + with caplog.at_level("ERROR"): + loads(qasm_str).validate() + + assert "Error at line 4, column 4" in caplog.text + assert f"def {keyword}" in caplog.text -@pytest.mark.parametrize("qubit_params", ["q", "q[:2]", "q[{0,1}]"]) -def test_qubit_size_arg_mismatch(qubit_params): +@pytest.mark.parametrize("qubit_params", ["q", "q[:2]", "q[{0, 1}]"]) +def test_qubit_size_arg_mismatch(qubit_params, caplog): """Test that passing a qubit of different size raises error.""" qasm_str = ( """OPENQASM 3.0; @@ -367,11 +375,19 @@ def my_function(qubit[3] q) { match="Qubit register size mismatch for function 'my_function'. " "Expected 3 qubits in variable 'q' but got 2", ): - loads(qasm_str).validate() + with caplog.at_level("ERROR"): + loads(qasm_str).validate() + + assert "Error at line 9, column 4" in caplog.text + assert f"my_function({qubit_params})" in caplog.text @pytest.mark.parametrize("test_name", SUBROUTINE_INCORRECT_TESTS.keys()) -def test_incorrect_custom_ops(test_name): - qasm_str, error_message = SUBROUTINE_INCORRECT_TESTS[test_name] +def test_incorrect_custom_ops(test_name, caplog): + qasm_str, error_message, line_num, col_num, err_line = SUBROUTINE_INCORRECT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - loads(qasm_str).validate() + with caplog.at_level("ERROR"): + loads(qasm_str).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text diff --git a/tests/qasm3/subroutines/test_subroutines_arrays.py b/tests/qasm3/subroutines/test_subroutines_arrays.py index 71ea190..1b533fa 100644 --- a/tests/qasm3/subroutines/test_subroutines_arrays.py +++ b/tests/qasm3/subroutines/test_subroutines_arrays.py @@ -159,7 +159,13 @@ def my_function(readonly array[int[8], 2, 2] my_arr1, @pytest.mark.parametrize("test_name", SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS.keys()) -def test_incorrect_custom_ops_with_arrays(test_name): - qasm_input, error_message = SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS[test_name] +def test_incorrect_custom_ops_with_arrays(test_name, caplog): + qasm_input, error_message, line_num, col_num, err_line = SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS[ + test_name + ] with pytest.raises(ValidationError, match=error_message): - loads(qasm_input).validate() + with caplog.at_level("ERROR"): + loads(qasm_input).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text From 698b64de358de8b32aca8ede8f21337034042e4e Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 21 Apr 2025 11:07:23 +0530 Subject: [PATCH 16/16] add changelog [no ci] --- CHANGELOG.md | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cc066..85e3eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,98 @@ barrier q1, q2, q3; barrier q2[:3]; barrier q3[0]; ``` +- Introduced a new environment variable called `PYQASM_EXPAND_TRACEBACK`. This variable can be set to `true` / `false` to enable / disable the expansion of traceback information in the error messages. The default is set as `false`. ([#171](https://github.com/qBraid/pyqasm/issues/171)) Eg. - + +**Script** - +```python +import pyqasm + +qasm = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q1; + rx(a) q1; + """ + +program = pyqasm.loads(qasm) +program.unroll() +``` + +**Execution** - +```bash +>>> python3 test-traceback.py +``` + +```bash +ERROR:pyqasm: Error at line 5, column 7 in QASM file + + >>>>>> a + +ERROR:pyqasm: Error at line 5, column 4 in QASM file + + >>>>>> rx(a) q1[0], q1[1]; + + +pyqasm.exceptions.ValidationError: Undefined identifier 'a' in expression + +The above exception was the direct cause of the following exception: + +pyqasm.exceptions.ValidationError: Invalid parameter 'a' for gate 'rx' +``` +```bash +>>> export PYQASM_EXPAND_TRACEBACK=true +``` + +```bash +>>> python3 test-traceback.py +``` + +```bash +ERROR:pyqasm: Error at line 5, column 7 in QASM file + + >>>>>> a + +ERROR:pyqasm: Error at line 5, column 4 in QASM file + + >>>>>> rx(a) q1[0], q1[1]; + + +Traceback (most recent call last): + ..... + + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/expressions.py", line 69, in _check_var_in_scope + raise_qasm3_error( + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/exceptions.py", line 103, in raise_qasm3_error + raise err_type(message) + +pyqasm.exceptions.ValidationError: Undefined identifier 'a' in expression + +The above exception was the direct cause of the following exception: + + +Traceback (most recent call last): + ..... + + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py", line 2208, in visit_basic_block + result.extend(self.visit_statement(stmt)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py", line 2188, in visit_statement + result.extend(visitor_function(statement)) # type: ignore[operator] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py", line 1201, in _visit_generic_gate_operation + result.extend(self._visit_basic_gate_operation(operation, inverse_value, ctrls)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py", line 820, in _visit_basic_gate_operation + op_parameters = self._get_op_parameters(operation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py", line 660, in _get_op_parameters + raise_qasm3_error( + File "/Users/thegupta/Desktop/qBraid/repos/pyqasm/src/pyqasm/exceptions.py", line 102, in raise_qasm3_error + raise err_type(message) from raised_from + +pyqasm.exceptions.ValidationError: Invalid parameter 'a' for gate 'rx' +``` + ### Improved / Modified - Improved the error messages for the parameter mismatch errors in basic quantum gates ([#169](https://github.com/qBraid/pyqasm/issues/169)). Following error is raised on parameter count mismatch - @@ -67,6 +159,41 @@ In [1]: import pyqasm ...... ValidationError: Expected 1 parameter for gate 'rx', but got 2 ``` + +- Enhanced the verbosity and clarity of `pyqasm` validation error messages. The new error format logs the line and column number of the error, the line where the error occurred, and the specific error message, making it easier to identify and fix issues in the QASM code. ([#171](https://github.com/qBraid/pyqasm/issues/171)) Eg. - + +```python +import pyqasm + +qasm = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q1; + rx(a) q1; + """ + +program = pyqasm.loads(qasm) +program.unroll() +``` + +```bash +ERROR:pyqasm: Error at line 5, column 7 in QASM file + + >>>>>> a + +ERROR:pyqasm: Error at line 5, column 4 in QASM file + + >>>>>> rx(a) q1[0], q1[1]; + + +pyqasm.exceptions.ValidationError: Undefined identifier 'a' in expression + +The above exception was the direct cause of the following exception: + +pyqasm.exceptions.ValidationError: Invalid parameter 'a' for gate 'rx' +``` + + ### Deprecated ### Removed