Skip to content
Merged
7 changes: 5 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Ref : https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#per-repository-settings

# Set line endings to lf
* text eol=lf
# Set line endings to lf for text files
* text eol=lf

# Binary files should not be processed as text
*.png binary
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ Types of changes:
- A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214))
- Added `unroll` command support in PYQASM CLI with options skipping files, overwriting originals files, and specifying output paths.([#224](https://github.com/qBraid/pyqasm/pull/224))
- Added `.github/copilot-instructions.md` to the repository to document coding standards and design principles for pyqasm. This file provides detailed guidance on documentation, static typing, formatting, error handling, and adherence to the QASM specification for all code contributions. ([#234](https://github.com/qBraid/pyqasm/pull/234))
- Added support for `Angle`,`extern` and `Complex` type in `OPENQASM3` code in pyqasm. ([#239](https://github.com/qBraid/pyqasm/pull/239))
###### Example:
```qasm
OPENQASM 3.0;
include "stdgates.inc";
angle[8] ang1;
ang1 = 9 * (pi / 8);
angle[8] ang1 = 7 * (pi / 8);
angle[8] ang3 = ang1 + ang2;
complex c1 = -2.5 - 3.5im;
const complex c2 = 2.0+arccos(π/2) + (3.1 * 5.5im);
const complex c12 = c1 * c2;
float a = 1.0;
int b = 2;
extern func1(float, int) -> bit;
bit c = 2 * func1(a, b);
bit fc = -func1(a, b);
bit[4] bd = "0101";
extern func6(bit[4]) -> bit[4];
bit[4] be1 = func6(bd);
```
- Added a new `QasmModule.compare` method to compare two QASM modules, providing a detailed report of differences in gates, qubits, and measurements. This method is useful for comparing two identifying differences in QASM programs, their structure and operations. ([#233](https://github.com/qBraid/pyqasm/pull/233))

### Improved / Modified
Expand Down
6 changes: 3 additions & 3 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ Source code for OpenQASM 3 program validator and semantic analyzer
| Box | ✅ | Completed |
| CalibrationStatement | 📋 | Planned |
| CalibrationDefinition | 📋 | Planned |
| ComplexType | 📋 | Planned |
| AngleType | 📋 | Planned |
| ExternDeclaration | 📋 | Planned |
| ComplexType | | Completed |
| AngleType | | Completed |
| ExternDeclaration | | Completed |
2 changes: 2 additions & 0 deletions src/pyqasm/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class Variable: # pylint: disable=too-many-instance-attributes
value (Optional[int | float | np.ndarray]): Value of the variable.
time_unit (Optional[str]): Time unit associated with the duration variable.
span (Any): Span of the variable.
angle_bit_string (Optional[str]): Bit string representation of the angle value.
shadow (bool): Flag indicating if the current variable is shadowed from its parent scope.
is_constant (bool): Flag indicating if the variable is constant.
is_register (bool): Flag indicating if the variable is a register.
Expand All @@ -106,6 +107,7 @@ class Variable: # pylint: disable=too-many-instance-attributes
value: Optional[int | float | np.ndarray] = None
time_unit: Optional[str] = None
span: Any = None
angle_bit_string: Optional[str] = None
shadow: bool = False
is_constant: bool = False
is_qubit: bool = False
Expand Down
6 changes: 6 additions & 0 deletions src/pyqasm/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule:
**kwargs: Additional arguments to pass to the loads function.
device_qubits (int): Number of physical qubits available on the target device.
device_cycle_time (float): The duration of a hardware device cycle, in seconds.
compiler_angle_type_size (int): The width of the angle type in the compiler.
extern_functions (dict): Dictionary of extern functions to be added to the module.

Raises:
TypeError: If the input is not a string or an `openqasm3.ast.Program` instance.
Expand Down Expand Up @@ -91,6 +93,10 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule:
module._device_qubits = dev_qbts
if dev_cycle_time := kwargs.get("device_cycle_time"):
module._device_cycle_time = dev_cycle_time
if compiler_angle_type_size := kwargs.get("compiler_angle_type_size"):
module._compiler_angle_type_size = compiler_angle_type_size
if extern_functions := kwargs.get("extern_functions"):
module._extern_functions = extern_functions
return module


Expand Down
139 changes: 122 additions & 17 deletions src/pyqasm/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

"""
from openqasm3.ast import (
AngleType,
BinaryExpression,
BitstringLiteral,
BitType,
BooleanLiteral,
BoolType,
Expand Down Expand Up @@ -45,14 +47,20 @@
from pyqasm.analyzer import Qasm3Analyzer
from pyqasm.elements import Variable
from pyqasm.exceptions import ValidationError, raise_qasm3_error
from pyqasm.maps.expressions import CONSTANTS_MAP, TIME_UNITS_MAP, qasm3_expression_op_map
from pyqasm.maps.expressions import (
CONSTANTS_MAP,
FUNCTION_MAP,
TIME_UNITS_MAP,
qasm3_expression_op_map,
)
from pyqasm.validator import Qasm3Validator


class Qasm3ExprEvaluator:
"""Class for evaluating QASM3 expressions."""

visitor_obj = None
angle_var_in_expr = None

@classmethod
def set_visitor_obj(cls, visitor_obj) -> None:
Expand All @@ -70,8 +78,8 @@ def _check_var_in_scope(cls, var_name, expression):
"""

scope_manager = cls.visitor_obj._scope_manager
var = scope_manager.get_from_global_scope(var_name)
if not scope_manager.check_in_scope(var_name):
var = scope_manager.get_from_global_scope(var_name)
if var is not None and not var.is_constant:
raise_qasm3_error(
f"Global variable '{var_name}' must be a constant to use it in a local scope.",
Expand All @@ -84,6 +92,14 @@ def _check_var_in_scope(cls, var_name, expression):
error_node=expression,
span=expression.span,
)
if var and isinstance(var.base_type, AngleType):
if cls.angle_var_in_expr and cls.angle_var_in_expr != var.base_type.size:
raise_qasm3_error(
"All 'Angle' variables in binary expression must have the same size",
error_node=expression,
span=expression.span,
)
cls.angle_var_in_expr = var.base_type.size

@classmethod
def _check_var_constant(cls, var_name, const_expr, expression):
Expand Down Expand Up @@ -192,6 +208,8 @@ def evaluate_expression( # type: ignore[return]
expression (Any): The expression to evaluate.
const_expr (bool): Whether the expression is a constant. Defaults to False.
reqd_type (Any): The required type of the expression. Defaults to None.
validate_only (bool): Whether to validate the expression only. Defaults to False.
dt (float): The time step of the compiler. Defaults to None.

Returns:
tuple[Any, list[Statement]] : The result of the evaluation.
Expand All @@ -203,14 +221,6 @@ def evaluate_expression( # type: ignore[return]
if expression is None:
return None, []

if isinstance(expression, (ImaginaryLiteral)):
raise_qasm3_error(
f"Unsupported expression type '{type(expression)}'",
err_type=ValidationError,
error_node=expression,
span=expression.span,
)

def _check_and_return_value(value):
if validate_only:
return None, statements
Expand Down Expand Up @@ -251,10 +261,25 @@ def _check_type_size(expression, var_name, var_format, base_type):
)
return base_size

def _is_external_function_call(expression):
"""Check if an expression is an external function call"""
return isinstance(expression, FunctionCall) and (
expression.name.name in cls.visitor_obj._module._extern_functions
)

def _get_external_function_return_type(expression):
"""Get the return type of an external function call"""
if _is_external_function_call(expression):
return cls.visitor_obj._module._extern_functions[expression.name.name][1]
return None

if isinstance(expression, ImaginaryLiteral):
return _check_and_return_value(expression.value * 1j)

if isinstance(expression, Identifier):
var_name = expression.name
if var_name in CONSTANTS_MAP:
if not reqd_type or reqd_type == Qasm3FloatType:
if not reqd_type or reqd_type in (Qasm3FloatType, AngleType):
return _check_and_return_value(CONSTANTS_MAP[var_name])
raise_qasm3_error(
f"Constant '{var_name}' not allowed in non-float expression",
Expand Down Expand Up @@ -318,6 +343,8 @@ def _check_type_size(expression, var_name, var_format, base_type):
return _check_and_return_value(expression.value)
if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral):
return _check_and_return_value(expression.value)
if reqd_type == AngleType:
return _check_and_return_value(expression.value)
raise_qasm3_error(
f"Invalid value {expression.value} with type {type(expression)} "
f"for required type {reqd_type}",
Expand All @@ -327,6 +354,9 @@ def _check_type_size(expression, var_name, var_format, base_type):
)
return _check_and_return_value(expression.value)

if isinstance(expression, BitstringLiteral):
return _check_and_return_value(format(expression.value, f"0{expression.width}b"))

if isinstance(expression, DurationLiteral):
unit_name = expression.unit.name
if dt:
Expand All @@ -345,11 +375,21 @@ def _check_type_size(expression, var_name, var_format, base_type):
return cls.evaluate_expression(
expression.expression, const_expr, reqd_type, validate_only
)
# Check for external function in validate_only mode
return_type = _get_external_function_return_type(expression.expression)
if return_type:
return (return_type, statements)
return (None, [])

operand, returned_stats = cls.evaluate_expression(
expression.expression, const_expr, reqd_type
)

# Handle external function replacement
if _is_external_function_call(expression.expression):
expression.expression = returned_stats[0]
return _check_and_return_value(None)

if expression.op.name == "~" and not isinstance(operand, int):
raise_qasm3_error(
f"Unsupported expression type '{type(operand)}' in ~ operation",
Expand All @@ -365,23 +405,75 @@ def _check_type_size(expression, var_name, var_format, base_type):
if validate_only:
if isinstance(expression.lhs, Cast) and isinstance(expression.rhs, Cast):
return (None, statements)

_lhs, _lhs_stmts = cls.evaluate_expression(
expression.lhs,
const_expr,
reqd_type,
validate_only,
)
_rhs, _rhs_stmts = cls.evaluate_expression(
expression.rhs,
const_expr,
reqd_type,
validate_only,
)

if isinstance(expression.lhs, Cast):
return cls.evaluate_expression(
expression.lhs, const_expr, reqd_type, validate_only
)
return (_lhs, _lhs_stmts)
if isinstance(expression.rhs, Cast):
return cls.evaluate_expression(
expression.rhs, const_expr, reqd_type, validate_only
)
return (_rhs, _rhs_stmts)

if type(reqd_type) is type(AngleType) and cls.angle_var_in_expr:
_var_type = AngleType(cls.angle_var_in_expr)
cls.angle_var_in_expr = None
return (_var_type, statements)

_lhs_return_type = None
_rhs_return_type = None
# Check for external functions in both operands
_lhs_return_type = _get_external_function_return_type(expression.lhs)
_rhs_return_type = _get_external_function_return_type(expression.rhs)

if _lhs_return_type and _rhs_return_type:
if _lhs_return_type != _rhs_return_type:
raise_qasm3_error(
f"extern function return type mismatch in binary expression: "
f"{type(_lhs_return_type).__name__} and "
f"{type(_rhs_return_type).__name__}",
err_type=ValidationError,
error_node=expression,
span=expression.span,
)
else:
if _lhs_return_type:
return (_lhs_return_type, statements)
if _rhs_return_type:
return (_rhs_return_type, statements)

return (None, statements)

lhs_value, lhs_statements = cls.evaluate_expression(
expression.lhs, const_expr, reqd_type
)
# Handle external function replacement for lhs
lhs_extern_function = False
if _is_external_function_call(expression.lhs):
expression.lhs = lhs_statements[0]
lhs_extern_function = True
statements.extend(lhs_statements)

rhs_value, rhs_statements = cls.evaluate_expression(
expression.rhs, const_expr, reqd_type
)
# Handle external function replacement for rhs
rhs_extern_function = False
if _is_external_function_call(expression.rhs):
expression.rhs = rhs_statements[0]
rhs_extern_function = True
if lhs_extern_function or rhs_extern_function:
return (None, [])

statements.extend(rhs_statements)
return _check_and_return_value(
qasm3_expression_op_map(expression.op.name, lhs_value, rhs_value)
Expand All @@ -390,6 +482,19 @@ def _check_type_size(expression, var_name, var_format, base_type):
if isinstance(expression, FunctionCall):
# function will not return a reqd / const type
# Reference : https://openqasm.com/language/types.html#compile-time-constants, para: 5
if validate_only:
return_type = _get_external_function_return_type(expression)
if return_type:
return (return_type, statements)
return (None, statements)

if expression.name.name in FUNCTION_MAP:
_val, _ = cls.evaluate_expression(
expression.arguments[0], const_expr, reqd_type, validate_only
)
_val = FUNCTION_MAP[expression.name.name](_val) # type: ignore
return _check_and_return_value(_val)

ret_value, ret_stmts = cls.visitor_obj._visit_function_call(expression) # type: ignore
statements.extend(ret_stmts)
return _check_and_return_value(ret_value)
Expand Down
Loading
Loading