Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
79 changes: 46 additions & 33 deletions src/pyqasm/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)

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

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

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

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