Skip to content

Commit 5d8dc97

Browse files
authored
Added support to Angle, extern and Complex type (#239)
* fix the `.gitattributes` file to exclude binary files from text processing * Add angle type support in QASM * update CHANGELOG.md * update test case * Add support for complex number operations in QASM * update CHANGELOG.md * code refactor - Changed `angle_vars_in_expr` to a single variable `angle_var_in_expr` for improved clarity and functionality. - Introduced new trigonometric functions (`arccos`, `arcsin`, `arctan`) to the FUNCTION_MAP. - Adjusted tests to validate new angle expressions and ensure correct bit string conversions. * Add extern function support in QASM * Enhance QASM type validation and bitstring handling - Updated `Qasm3ExprEvaluator` to include support for `BitstringLiteral` in expression evaluations. - Improved `Qasm3SubroutineProcessor` to validate argument types, including checks for `BitType` and `AngleType`. - Added a new method in `QasmVisitor` to validate the width of bitstring literals. - Adjusted `PulseValidator` to handle string representations of bitstrings. - Updated tests to reflect changes in bitstring handling and validation logic. * code refactor * update test case * code refactor
1 parent f496be9 commit 5d8dc97

16 files changed

Lines changed: 1207 additions & 95 deletions

File tree

.gitattributes

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Ref : https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#per-repository-settings
22

3-
# Set line endings to lf
4-
* text eol=lf
3+
# Set line endings to lf for text files
4+
* text eol=lf
5+
6+
# Binary files should not be processed as text
7+
*.png binary

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@ Types of changes:
1919
- A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214))
2020
- 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))
2121
- 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))
22+
- Added support for `Angle`,`extern` and `Complex` type in `OPENQASM3` code in pyqasm. ([#239](https://github.com/qBraid/pyqasm/pull/239))
23+
###### Example:
24+
```qasm
25+
OPENQASM 3.0;
26+
include "stdgates.inc";
27+
angle[8] ang1;
28+
ang1 = 9 * (pi / 8);
29+
angle[8] ang1 = 7 * (pi / 8);
30+
angle[8] ang3 = ang1 + ang2;
31+
32+
complex c1 = -2.5 - 3.5im;
33+
const complex c2 = 2.0+arccos(π/2) + (3.1 * 5.5im);
34+
const complex c12 = c1 * c2;
35+
36+
float a = 1.0;
37+
int b = 2;
38+
extern func1(float, int) -> bit;
39+
bit c = 2 * func1(a, b);
40+
bit fc = -func1(a, b);
41+
42+
bit[4] bd = "0101";
43+
extern func6(bit[4]) -> bit[4];
44+
bit[4] be1 = func6(bd);
45+
```
2246
- 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))
2347

2448
### Improved / Modified

src/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ Source code for OpenQASM 3 program validator and semantic analyzer
3838
| Box || Completed |
3939
| CalibrationStatement | 📋 | Planned |
4040
| CalibrationDefinition | 📋 | Planned |
41-
| ComplexType | 📋 | Planned |
42-
| AngleType | 📋 | Planned |
43-
| ExternDeclaration | 📋 | Planned |
41+
| ComplexType | | Completed |
42+
| AngleType | | Completed |
43+
| ExternDeclaration | | Completed |

src/pyqasm/elements.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class Variable: # pylint: disable=too-many-instance-attributes
9292
value (Optional[int | float | np.ndarray]): Value of the variable.
9393
time_unit (Optional[str]): Time unit associated with the duration variable.
9494
span (Any): Span of the variable.
95+
angle_bit_string (Optional[str]): Bit string representation of the angle value.
9596
shadow (bool): Flag indicating if the current variable is shadowed from its parent scope.
9697
is_constant (bool): Flag indicating if the variable is constant.
9798
is_register (bool): Flag indicating if the variable is a register.
@@ -106,6 +107,7 @@ class Variable: # pylint: disable=too-many-instance-attributes
106107
value: Optional[int | float | np.ndarray] = None
107108
time_unit: Optional[str] = None
108109
span: Any = None
110+
angle_bit_string: Optional[str] = None
109111
shadow: bool = False
110112
is_constant: bool = False
111113
is_qubit: bool = False

src/pyqasm/entrypoint.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule:
5959
**kwargs: Additional arguments to pass to the loads function.
6060
device_qubits (int): Number of physical qubits available on the target device.
6161
device_cycle_time (float): The duration of a hardware device cycle, in seconds.
62+
compiler_angle_type_size (int): The width of the angle type in the compiler.
63+
extern_functions (dict): Dictionary of extern functions to be added to the module.
6264
6365
Raises:
6466
TypeError: If the input is not a string or an `openqasm3.ast.Program` instance.
@@ -91,6 +93,10 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule:
9193
module._device_qubits = dev_qbts
9294
if dev_cycle_time := kwargs.get("device_cycle_time"):
9395
module._device_cycle_time = dev_cycle_time
96+
if compiler_angle_type_size := kwargs.get("compiler_angle_type_size"):
97+
module._compiler_angle_type_size = compiler_angle_type_size
98+
if extern_functions := kwargs.get("extern_functions"):
99+
module._extern_functions = extern_functions
94100
return module
95101

96102

src/pyqasm/expressions.py

Lines changed: 122 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
1818
"""
1919
from openqasm3.ast import (
20+
AngleType,
2021
BinaryExpression,
22+
BitstringLiteral,
2123
BitType,
2224
BooleanLiteral,
2325
BoolType,
@@ -45,14 +47,20 @@
4547
from pyqasm.analyzer import Qasm3Analyzer
4648
from pyqasm.elements import Variable
4749
from pyqasm.exceptions import ValidationError, raise_qasm3_error
48-
from pyqasm.maps.expressions import CONSTANTS_MAP, TIME_UNITS_MAP, qasm3_expression_op_map
50+
from pyqasm.maps.expressions import (
51+
CONSTANTS_MAP,
52+
FUNCTION_MAP,
53+
TIME_UNITS_MAP,
54+
qasm3_expression_op_map,
55+
)
4956
from pyqasm.validator import Qasm3Validator
5057

5158

5259
class Qasm3ExprEvaluator:
5360
"""Class for evaluating QASM3 expressions."""
5461

5562
visitor_obj = None
63+
angle_var_in_expr = None
5664

5765
@classmethod
5866
def set_visitor_obj(cls, visitor_obj) -> None:
@@ -70,8 +78,8 @@ def _check_var_in_scope(cls, var_name, expression):
7078
"""
7179

7280
scope_manager = cls.visitor_obj._scope_manager
81+
var = scope_manager.get_from_global_scope(var_name)
7382
if not scope_manager.check_in_scope(var_name):
74-
var = scope_manager.get_from_global_scope(var_name)
7583
if var is not None and not var.is_constant:
7684
raise_qasm3_error(
7785
f"Global variable '{var_name}' must be a constant to use it in a local scope.",
@@ -84,6 +92,14 @@ def _check_var_in_scope(cls, var_name, expression):
8492
error_node=expression,
8593
span=expression.span,
8694
)
95+
if var and isinstance(var.base_type, AngleType):
96+
if cls.angle_var_in_expr and cls.angle_var_in_expr != var.base_type.size:
97+
raise_qasm3_error(
98+
"All 'Angle' variables in binary expression must have the same size",
99+
error_node=expression,
100+
span=expression.span,
101+
)
102+
cls.angle_var_in_expr = var.base_type.size
87103

88104
@classmethod
89105
def _check_var_constant(cls, var_name, const_expr, expression):
@@ -192,6 +208,8 @@ def evaluate_expression( # type: ignore[return]
192208
expression (Any): The expression to evaluate.
193209
const_expr (bool): Whether the expression is a constant. Defaults to False.
194210
reqd_type (Any): The required type of the expression. Defaults to None.
211+
validate_only (bool): Whether to validate the expression only. Defaults to False.
212+
dt (float): The time step of the compiler. Defaults to None.
195213
196214
Returns:
197215
tuple[Any, list[Statement]] : The result of the evaluation.
@@ -203,14 +221,6 @@ def evaluate_expression( # type: ignore[return]
203221
if expression is None:
204222
return None, []
205223

206-
if isinstance(expression, (ImaginaryLiteral)):
207-
raise_qasm3_error(
208-
f"Unsupported expression type '{type(expression)}'",
209-
err_type=ValidationError,
210-
error_node=expression,
211-
span=expression.span,
212-
)
213-
214224
def _check_and_return_value(value):
215225
if validate_only:
216226
return None, statements
@@ -251,10 +261,25 @@ def _check_type_size(expression, var_name, var_format, base_type):
251261
)
252262
return base_size
253263

264+
def _is_external_function_call(expression):
265+
"""Check if an expression is an external function call"""
266+
return isinstance(expression, FunctionCall) and (
267+
expression.name.name in cls.visitor_obj._module._extern_functions
268+
)
269+
270+
def _get_external_function_return_type(expression):
271+
"""Get the return type of an external function call"""
272+
if _is_external_function_call(expression):
273+
return cls.visitor_obj._module._extern_functions[expression.name.name][1]
274+
return None
275+
276+
if isinstance(expression, ImaginaryLiteral):
277+
return _check_and_return_value(expression.value * 1j)
278+
254279
if isinstance(expression, Identifier):
255280
var_name = expression.name
256281
if var_name in CONSTANTS_MAP:
257-
if not reqd_type or reqd_type == Qasm3FloatType:
282+
if not reqd_type or reqd_type in (Qasm3FloatType, AngleType):
258283
return _check_and_return_value(CONSTANTS_MAP[var_name])
259284
raise_qasm3_error(
260285
f"Constant '{var_name}' not allowed in non-float expression",
@@ -318,6 +343,8 @@ def _check_type_size(expression, var_name, var_format, base_type):
318343
return _check_and_return_value(expression.value)
319344
if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral):
320345
return _check_and_return_value(expression.value)
346+
if reqd_type == AngleType:
347+
return _check_and_return_value(expression.value)
321348
raise_qasm3_error(
322349
f"Invalid value {expression.value} with type {type(expression)} "
323350
f"for required type {reqd_type}",
@@ -327,6 +354,9 @@ def _check_type_size(expression, var_name, var_format, base_type):
327354
)
328355
return _check_and_return_value(expression.value)
329356

357+
if isinstance(expression, BitstringLiteral):
358+
return _check_and_return_value(format(expression.value, f"0{expression.width}b"))
359+
330360
if isinstance(expression, DurationLiteral):
331361
unit_name = expression.unit.name
332362
if dt:
@@ -345,11 +375,21 @@ def _check_type_size(expression, var_name, var_format, base_type):
345375
return cls.evaluate_expression(
346376
expression.expression, const_expr, reqd_type, validate_only
347377
)
378+
# Check for external function in validate_only mode
379+
return_type = _get_external_function_return_type(expression.expression)
380+
if return_type:
381+
return (return_type, statements)
348382
return (None, [])
349383

350384
operand, returned_stats = cls.evaluate_expression(
351385
expression.expression, const_expr, reqd_type
352386
)
387+
388+
# Handle external function replacement
389+
if _is_external_function_call(expression.expression):
390+
expression.expression = returned_stats[0]
391+
return _check_and_return_value(None)
392+
353393
if expression.op.name == "~" and not isinstance(operand, int):
354394
raise_qasm3_error(
355395
f"Unsupported expression type '{type(operand)}' in ~ operation",
@@ -365,23 +405,75 @@ def _check_type_size(expression, var_name, var_format, base_type):
365405
if validate_only:
366406
if isinstance(expression.lhs, Cast) and isinstance(expression.rhs, Cast):
367407
return (None, statements)
408+
409+
_lhs, _lhs_stmts = cls.evaluate_expression(
410+
expression.lhs,
411+
const_expr,
412+
reqd_type,
413+
validate_only,
414+
)
415+
_rhs, _rhs_stmts = cls.evaluate_expression(
416+
expression.rhs,
417+
const_expr,
418+
reqd_type,
419+
validate_only,
420+
)
421+
368422
if isinstance(expression.lhs, Cast):
369-
return cls.evaluate_expression(
370-
expression.lhs, const_expr, reqd_type, validate_only
371-
)
423+
return (_lhs, _lhs_stmts)
372424
if isinstance(expression.rhs, Cast):
373-
return cls.evaluate_expression(
374-
expression.rhs, const_expr, reqd_type, validate_only
375-
)
425+
return (_rhs, _rhs_stmts)
426+
427+
if type(reqd_type) is type(AngleType) and cls.angle_var_in_expr:
428+
_var_type = AngleType(cls.angle_var_in_expr)
429+
cls.angle_var_in_expr = None
430+
return (_var_type, statements)
431+
432+
_lhs_return_type = None
433+
_rhs_return_type = None
434+
# Check for external functions in both operands
435+
_lhs_return_type = _get_external_function_return_type(expression.lhs)
436+
_rhs_return_type = _get_external_function_return_type(expression.rhs)
437+
438+
if _lhs_return_type and _rhs_return_type:
439+
if _lhs_return_type != _rhs_return_type:
440+
raise_qasm3_error(
441+
f"extern function return type mismatch in binary expression: "
442+
f"{type(_lhs_return_type).__name__} and "
443+
f"{type(_rhs_return_type).__name__}",
444+
err_type=ValidationError,
445+
error_node=expression,
446+
span=expression.span,
447+
)
448+
else:
449+
if _lhs_return_type:
450+
return (_lhs_return_type, statements)
451+
if _rhs_return_type:
452+
return (_rhs_return_type, statements)
453+
376454
return (None, statements)
377455

378456
lhs_value, lhs_statements = cls.evaluate_expression(
379457
expression.lhs, const_expr, reqd_type
380458
)
459+
# Handle external function replacement for lhs
460+
lhs_extern_function = False
461+
if _is_external_function_call(expression.lhs):
462+
expression.lhs = lhs_statements[0]
463+
lhs_extern_function = True
381464
statements.extend(lhs_statements)
465+
382466
rhs_value, rhs_statements = cls.evaluate_expression(
383467
expression.rhs, const_expr, reqd_type
384468
)
469+
# Handle external function replacement for rhs
470+
rhs_extern_function = False
471+
if _is_external_function_call(expression.rhs):
472+
expression.rhs = rhs_statements[0]
473+
rhs_extern_function = True
474+
if lhs_extern_function or rhs_extern_function:
475+
return (None, [])
476+
385477
statements.extend(rhs_statements)
386478
return _check_and_return_value(
387479
qasm3_expression_op_map(expression.op.name, lhs_value, rhs_value)
@@ -390,6 +482,19 @@ def _check_type_size(expression, var_name, var_format, base_type):
390482
if isinstance(expression, FunctionCall):
391483
# function will not return a reqd / const type
392484
# Reference : https://openqasm.com/language/types.html#compile-time-constants, para: 5
485+
if validate_only:
486+
return_type = _get_external_function_return_type(expression)
487+
if return_type:
488+
return (return_type, statements)
489+
return (None, statements)
490+
491+
if expression.name.name in FUNCTION_MAP:
492+
_val, _ = cls.evaluate_expression(
493+
expression.arguments[0], const_expr, reqd_type, validate_only
494+
)
495+
_val = FUNCTION_MAP[expression.name.name](_val) # type: ignore
496+
return _check_and_return_value(_val)
497+
393498
ret_value, ret_stmts = cls.visitor_obj._visit_function_call(expression) # type: ignore
394499
statements.extend(ret_stmts)
395500
return _check_and_return_value(ret_value)

0 commit comments

Comments
 (0)