8000 Improve error reporting for pyqasm by TheGupta2012 · Pull Request #171 · qBraid/pyqasm · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Improve error reporting for pyqasm #171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

8000

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 21, 2025
127 changes: 127 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/pyqasm/_logging.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 19 additions & 13 deletions src/pyqasm/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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,
)

Expand All @@ -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))

Expand Down Expand Up @@ -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,
)
1 change: 1 addition & 0 deletions src/pyqasm/cli/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down
39 changes: 35 additions & 4 deletions src/pyqasm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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)
Loading
0