diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cc0662..85e3eac9 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 diff --git a/src/pyqasm/_logging.py b/src/pyqasm/_logging.py new file mode 100644 index 00000000..01310a9c --- /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/analyzer.py b/src/pyqasm/analyzer.py index 3ce6ff72..648fd7a4 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,34 +81,38 @@ 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, ) - 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}", + f"of variable '{var_name}'. Expected index in range [0, {dimension-1}]", err_type=ValidationError, - 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, - span=span, + error_node=index_node, + span=index_node.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, + error_node=index, span=index.span, ) @@ -126,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)) @@ -283,6 +288,7 @@ 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}", + error_node=gate, span=span, ) diff --git a/src/pyqasm/cli/validate.py b/src/pyqasm/cli/validate.py index 5bae1ec3..c1ad09f3 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 119a43c1..7a465c09 100644 --- a/src/pyqasm/exceptions.py +++ b/src/pyqasm/exceptions.py @@ -17,11 +17,15 @@ """ -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 + +from ._logging import logger class PyQasmError(Exception): @@ -48,6 +52,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 +61,43 @@ 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: + 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) + ) + except Exception as _: # pylint: disable = broad-exception-caught + print(_) + error_parts.append("\n >>>>>> " + str(error_node)) + + if error_parts: + logger.error("\n".join(error_parts)) + + 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: raise err_type(message) from raised_from raise err_type(message) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 63e66267..f89ad620 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -67,9 +67,10 @@ 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, + f"Undefined identifier '{var_name}' in expression", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -88,9 +89,10 @@ 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", - ValidationError, - expression.span, + f"Expected variable '{var_name}' to be constant in given expression", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -106,14 +108,14 @@ 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}", - ValidationError, - expression.span, + message=f"Invalid type '{var.base_type}' of variable '{var_name}' for " + f"required type {reqd_type}", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @staticmethod @@ -130,9 +132,10 @@ 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, + f"Uninitialized variable '{var_name}' in expression", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) @classmethod @@ -183,9 +186,10 @@ def evaluate_expression( # type: ignore[return] if isinstance(expression, (ImaginaryLiteral, DurationLiteral)): 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, ) def _check_and_return_value(value): @@ -207,9 +211,10 @@ 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", - ValidationError, - expression.span, + f"Constant '{var_name}' not allowed in non-float expression", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) return _process_variable(var_name) @@ -229,15 +234,17 @@ 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, + error_node=expression, 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, + error_node=expression, span=expression.span, ) @@ -250,10 +257,11 @@ 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, + err_type=ValidationError, + error_node=expression, + span=expression.span, ) return _check_and_return_value(dimensions[index]) @@ -268,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,9 +288,10 @@ 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, + f"Unsupported expression type '{type(operand)}' in ~ operation", + err_type=ValidationError, + error_node=expression, + span=expression.span, ) op_name = "UMINUS" if expression.op.name == "-" else expression.op.name statements.extend(returned_stats) @@ -308,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/maps/gates.py b/src/pyqasm/maps/gates.py index 1de2da80..999c4828 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/subroutines.py b/src/pyqasm/subroutines.py index 34e0f54f..4f4096e3 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -24,12 +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 @@ -73,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: @@ -89,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. @@ -117,12 +119,21 @@ 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) + 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}'", - 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 @@ -130,8 +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}'", - 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] @@ -147,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. @@ -171,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')}" @@ -178,26 +193,34 @@ def _process_classical_arg_by_reference( f" in function '{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", - span=span, + + f"Literal '{Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]}' " + + "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", - 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}'", - 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) @@ -207,8 +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}'.", - 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 @@ -218,8 +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}'.", - 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 @@ -230,26 +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}'", - 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)}", - 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 = [] @@ -261,23 +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}'", - 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}", - 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 @@ -300,7 +349,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, @@ -308,7 +357,7 @@ def process_quantum_arg( duplicate_qubit_map, qubit_transform_map, fn_name, - span, + fn_call, ): """ Process a quantum argument in the QASM3 visitor. @@ -320,7 +369,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. @@ -337,11 +386,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}'", - span=span, + error_node=fn_defn.arguments, + span=formal_arg.span, ) formal_qreg_size_map[formal_reg_name] = formal_qubit_size @@ -349,10 +402,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}'", - span=span, + f"{arg_desc}found for function '{fn_name}'\n" + f"\nUsage: {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) @@ -361,11 +424,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}", - span=span, + f"Expected {formal_qubit_size} qubits in variable '{formal_reg_name}' " + f"but got {actual_qubits_size}\n" + f"\nUsage: {fn_name} ( {formal_args_desc} )\n", + error_node=fn_call, + span=fn_call.span, ) if not Qasm3Validator.validate_unique_qubits( @@ -374,7 +440,8 @@ def process_quantum_arg( raise_qasm3_error( f"Duplicate qubit argument for register '{actual_arg_name}' " f"in function call for '{fn_name}'", - 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 039dbaa0..119ac358 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: @@ -111,21 +114,27 @@ 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", - span=discrete_set.span, + f"Unsupported value '{Qasm3ExprEvaluator.evaluate_expression(value)[0]}' " + "in discrete set", + 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. """ @@ -144,8 +153,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=op_node + ) + Qasm3Validator.validate_register_index( + end_qid - 1, qreg_size, qubit=is_qubit_reg, op_node=op_node + ) return list(range(start_qid, end_qid, step)) @staticmethod @@ -167,7 +180,9 @@ 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}", + error_node=gate_op, span=qubit.span, ) gate_qubit_name = qubit.name @@ -256,12 +271,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( @@ -275,6 +292,7 @@ def get_branch_params( raise_qasm3_error( message="Only {==, >=, <=, >, <} supported in branching condition " "with classical register", + error_node=condition, span=condition.span, ) @@ -300,12 +318,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( @@ -382,13 +402,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 048e2753..90d0ac44 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 @@ -67,7 +81,8 @@ 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", + error_node=statement, span=statement.span, ) @@ -75,6 +90,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, ) @@ -96,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: @@ -116,7 +134,13 @@ 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_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) @@ -136,8 +160,10 @@ 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}", + error_node=op_node, + span=op_node.span if op_node else None, ) elif type_to_match == float: @@ -150,37 +176,48 @@ 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}", + 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 @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 @@ -200,7 +237,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,8 +272,9 @@ 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, ) @@ -244,8 +282,9 @@ 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, ) @@ -274,13 +313,15 @@ 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: 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, ) base_size = 1 @@ -296,6 +337,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 1365aa9a..41fee124 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 @@ -44,6 +45,7 @@ from pyqasm.validator import Qasm3Validator logger = logging.getLogger(__name__) +logger.propagate = False # pylint: disable-next=too-many-instance-attributes @@ -275,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] @@ -291,12 +302,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, ) @@ -334,8 +347,13 @@ 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}'", + error_node=operation, + span=operation.span, ) # pylint: disable-next=too-many-locals,too-many-branches @@ -390,23 +408,33 @@ 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, + error_node=operation, span=operation.span, ) self._check_if_name_in_scope(reg_name, operation) 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] 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: @@ -453,8 +481,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, ) @@ -481,8 +509,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, ) @@ -494,6 +523,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, ) @@ -623,8 +653,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 @@ -639,7 +677,11 @@ 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}'", + error_node=definition, + span=definition.span, + ) self._custom_gates[gate_name] = definition return [] @@ -661,6 +703,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 = [] @@ -764,7 +807,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( @@ -782,6 +825,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, ) @@ -877,7 +921,9 @@ 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}'", + error_node=gate_op, + span=gate_op.span, ) Qasm3Transformer.transform_gate_params(gate_op_copy, param_map) Qasm3Transformer.transform_gate_qubits(gate_op_copy, qubit_map) @@ -891,7 +937,9 @@ 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__}'", + error_node=gate_op, + span=gate_op.span, ) self._restore_context() @@ -936,7 +984,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) @@ -1025,7 +1073,8 @@ 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", + "Qubit arguments not allowed for 'gphase' operation in global scope", + error_node=operation, span=operation.span, ) @@ -1087,6 +1136,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 @@ -1104,6 +1154,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: @@ -1112,6 +1163,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] @@ -1132,6 +1184,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, ) @@ -1176,13 +1229,28 @@ 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) - init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( - statement.init_expression, const_expr=True - ) + raise_qasm3_error( + f"Re-declaration of variable '{var_name}'", + error_node=statement, + span=statement.span, + ) + 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 @@ -1192,19 +1260,28 @@ 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) # 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) @@ -1229,7 +1306,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(): @@ -1239,7 +1318,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 @@ -1253,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): @@ -1269,12 +1360,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"Invalid dimensions {len(dimensions)} for array declaration for '{var_name}'. " f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}", + error_node=statement, span=statement.span, ) @@ -1282,7 +1376,8 @@ 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}'", + error_node=statement, span=statement.span, ) final_dimensions.append(dim_value) @@ -1301,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, @@ -1319,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 - ) + 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) @@ -1368,10 +1490,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["="]: @@ -1396,7 +1524,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) @@ -1412,6 +1540,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, ) @@ -1422,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, @@ -1484,7 +1620,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 @@ -1496,7 +1632,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, ) @@ -1508,7 +1645,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( @@ -1594,6 +1731,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] @@ -1612,8 +1750,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 @@ -1664,16 +1804,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, ) @@ -1695,7 +1840,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] @@ -1703,6 +1852,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, ) @@ -1715,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: @@ -1727,7 +1877,7 @@ def _visit_function_call( duplicate_qubit_detect_map, qubit_transform_map, fn_name, - statement.span, + statement, ) ) @@ -1810,7 +1960,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): @@ -1820,11 +1974,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;" @@ -1833,10 +1991,13 @@ 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, 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) @@ -1845,12 +2006,13 @@ 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];" 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, @@ -1897,13 +2059,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 @@ -1939,7 +2107,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) @@ -1966,7 +2136,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 [] @@ -2016,7 +2188,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/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..897961ad --- /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 diff --git a/tests/qasm3/declarations/test_classical.py b/tests/qasm3/declarations/test_classical.py index 56f423e2..b600f6a2 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 7c82b33c..39c38a8a 100644 --- a/tests/qasm3/declarations/test_quantum.py +++ b/tests/qasm3/declarations/test_quantum.py @@ -125,63 +125,76 @@ 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"Variable 'N' is not a 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"Variable 'size' is not a 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;", + ), + ], +) +# 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): + 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/gates.py b/tests/qasm3/resources/gates.py index f15d36d9..9cac401f 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -255,7 +255,10 @@ def test_fixture(): qubit[2] q1; h q2; // undeclared register """, - "Missing register declaration for q2 .*", + "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": ( """ @@ -316,7 +334,10 @@ 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", + 6, + 8, + "cx q1[0], q1[0];", ), } @@ -328,7 +349,10 @@ def test_fixture(): qubit q; gphase(pi) q; """, - r"Qubit arguments not allowed for phase operation", + 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": ( """ @@ -353,7 +380,10 @@ 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.", + 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": ( """ @@ -382,7 +415,10 @@ 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.", + 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": ( """ @@ -431,6 +473,9 @@ 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'", + 10, + 8, + "gate custom_gate(a, b) p, q {", ), } diff --git a/tests/qasm3/resources/subroutines.py b/tests/qasm3/resources/subroutines.py index 83939eae..5d3dd8e8 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": ( """ @@ -55,7 +60,10 @@ def my_function(qubit q) { qubit q; my_function(q); """, - "Re-declaration of variable 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": ( """ @@ -184,7 +216,10 @@ 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'", + 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": ( """ @@ -271,7 +321,10 @@ 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", + 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 e0c31a82..2f1c7494 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": ( """ @@ -42,7 +47,10 @@ float y = 3.4; uint x; """, - "Re-declaration of variable 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": ( """ @@ -60,7 +71,10 @@ qubit x; int x; """, - "Re-declaration of variable x", + "Re-declaration of variable 'x'", + 5, + 8, + "int x;", ), "const_variable_redeclaration": ( """ @@ -69,7 +83,10 @@ const int x = 3; const float x = 3.4; """, - "Re-declaration of variable x", + "Re-declaration of variable 'x'", + 5, + 8, + "const float x = 3.4;", ), "invalid_int_size": ( """ @@ -77,7 +94,10 @@ include "stdgates.inc"; int[32.1] x; """, - "Invalid base size 32.1 for variable 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; """, - "Variable 'x' is not a 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; """, - "Variable 'x' is not a constant in given expression", + "Invalid base size for constant 'y'", + 5, + 8, + "const int[x] y = 5;", ), "invalid_float_size": ( """ @@ -112,7 +141,10 @@ float[23] x; """, - "Invalid base size 23 for float variable 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": ( """ @@ -139,7 +177,10 @@ 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'", + 5, + 8, + "array[int[32], 1, 2.1] x;", ), "extra_array_dimensions": ( """ @@ -148,7 +189,10 @@ 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", + 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", + "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/subroutines/test_subroutines.py b/tests/qasm3/subroutines/test_subroutines.py index 3862ea7d..bfbaec9b 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; @@ -365,13 +373,21 @@ 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() + 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 71ea190c..1b533fa9 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 diff --git a/tests/qasm3/test_alias.py b/tests/qasm3/test_alias.py index 669037d2..0ac8aad7 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 discrete set value .*", + 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=r"Variable alias not in scope for operation .*", + 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 7d122bd0..bbb1cb81 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; @@ -165,8 +165,16 @@ def test_incorrect_barrier(): barrier q2; """ - with pytest.raises(ValidationError, match=r"Missing register declaration for q2 .*"): - loads(undeclared).validate() + with pytest.raises( + ValidationError, match="Missing qubit register declaration for 'q2' in QuantumBarrier" + ): + 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; @@ -179,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 aa9bca18..9f023c5e 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(): - with pytest.raises(ValidationError, match=r"Unsupported expression type .*"): - loads("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;").validate() - - with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): - loads("OPENQASM 3; qubit q; rx(~1.3) q;").validate() - - 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"): - loads("OPENQASM 3; qubit q; rx(x) q;").validate() - - with pytest.raises(ValidationError, match="Uninitialized variable x in expression"): - loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate() +def test_incorrect_expressions(caplog): + 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 + assert "32.0im" in caplog.text + + caplog.clear() + + 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 + assert "~1.3" in caplog.text + + caplog.clear() + + 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 + assert "~1.3" in caplog.text + + caplog.clear() + + 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 + assert "x" in caplog.text + + caplog.clear() + + 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 + assert "x" in caplog.text diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 7df20f4b..c0e88a55 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 8e57fe28..5a06798d 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 d0fcb82a..ad7a3307 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 acb3142e..61ace6ee 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_reset.py b/tests/qasm3/test_reset.py index 44ed8a60..dafe71ff 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 04f70912..9ab82a68 100644 --- a/tests/qasm3/test_sizeof.py +++ b/tests/qasm3/test_sizeof.py @@ -79,47 +79,52 @@ 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 pytest.raises(ValidationError, match=r"Invalid initialization value for variable 'size1'"): + 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 pytest.raises(ValidationError, match="Invalid initialization value for variable 'size1'"): + 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 pytest.raises(ValidationError, match="Invalid initialization value for variable 'size1'"): + 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 d925d30b..6f362c1f 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,86 +371,91 @@ 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() - - -def test_non_int_expression_case(): - """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 ", - ): - 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() -def test_non_int_variable_expression(): - """Test that switch raises error if case expression has a non-int - variable in expression.""" + 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 - 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 .* for required type ", - ): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() - - -def test_non_constant_expression_case(): - """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"Variable .* is not a constant in given expression"): - qasm3_switch_program = base_invalid_program - loads(qasm3_switch_program).validate() +@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"): + loads(qasm3_code).validate() + + assert f"Error at line {line_num}, column {col_num}" in caplog.text + assert err_line in caplog.text