Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Types of changes:
### Added
- Negative indices are now honored across arrays, `bit[n]`, `qubit[n]`, and `let` aliases, including ranges: `myArray[-1]`, `a[-1] = 10`, `h q[-1]`, `bit c = b[-1]`, `let last_three = two[-4:-1]`. An index still outside `[-size, size)` after normalization raises `ValidationError` and names the index as written. ([#391](https://github.com/qBraid/pyqasm/issues/391))

- Added the built-in constant expression functions `ceiling`, `floor`, `exp`, `log`, `mod`, `popcount`, `rotl`, and `rotr`, each usable in a `const` initializer and as a gate argument. `rotl` and `rotr` preserve the operand's declared width, so `rotl(a, n) == rotr(a, -n)`. An unknown function name, a wrong argument count, or a wrong argument type now names the function instead of reporting only `Invalid initialization value`. `pow` is deliberately excluded: it is ambiguous with the gate modifier of the same name, and upstream removed it from the spec in [openqasm/openqasm#635](https://github.com/openqasm/openqasm/pull/635), leaving `**` as the supported spelling. ([#390](https://github.com/qBraid/pyqasm/issues/390))

### Improved / Modified

### Deprecated
Expand Down
10 changes: 10 additions & 0 deletions src/pyqasm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class ValidationError(PyQasmError):
"""Exception raised when a OpenQASM program fails validation."""


class FunctionCallError(ValidationError):
"""Exception raised when a function is called with an unknown name, the wrong
number of arguments, or an argument of the wrong type."""


class UnrollError(PyQasmError):
"""Exception raised when a OpenQASM program fails unrolling."""

Expand Down Expand Up @@ -133,5 +138,10 @@ def raise_qasm3_error(

# Extract the latest message from the traceback if raised_from is provided
if raised_from:
if isinstance(raised_from, FunctionCallError):
# Statement-level handlers wrap any evaluation failure in a generic message
# ("Invalid initialization value for constant 'c'"). Merge the function
# diagnostic in so the offending function is still named.
message = f"{message}: {raised_from}"
raise err_type(message) from raised_from
raise err_type(message)
73 changes: 68 additions & 5 deletions src/pyqasm/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@
SizeOf,
Statement,
StretchType,
UintType,
UnaryExpression,
)

from pyqasm.analyzer import Qasm3Analyzer, bits_to_int
from pyqasm.elements import BitValue, Variable
from pyqasm.exceptions import ValidationError, raise_qasm3_error
from pyqasm.exceptions import FunctionCallError, ValidationError, raise_qasm3_error
from pyqasm.maps.expressions import (
BIT_ROTATION_FUNCTIONS,
CONSTANTS_MAP,
FUNCTION_MAP,
TIME_UNITS_MAP,
Expand Down Expand Up @@ -210,6 +212,69 @@ def _get_var_value(cls, var_name, indices, expression): # pylint: disable=too-m

return Qasm3Analyzer.find_array_element(var.value, validated_indices)

@classmethod
def _as_bit_value( # type: ignore[return] # pylint: disable=inconsistent-return-statements
cls, value, expression
) -> BitValue:
"""Recover the register width of a ``rotl`` / ``rotr`` operand.

A rotation is only defined against a declared width, so an operand whose width
is unknown (a bare integer literal) is rejected rather than given a guessed one.
"""
if isinstance(value, BitValue):
return value
if isinstance(value, str):
return BitValue(int(value, 2) if value else 0, len(value))
argument = expression.arguments[0]
if isinstance(argument, Identifier) and isinstance(value, int):
var = cls.visitor_obj._scope_manager.get_from_visible_scope( # type: ignore[union-attr]
argument.name
)
if isinstance(var.base_type, (BitType, UintType)):
return BitValue(value, var.base_size)
raise_qasm3_error(
f"Function '{expression.name.name}' expects a 'bit[n]' or 'uint[n]' "
"operand of known width",
err_type=FunctionCallError,
error_node=expression,
span=expression.span,
)

@classmethod
def _evaluate_builtin_function( # pylint: disable=inconsistent-return-statements
cls, expression, const_expr, reqd_type
):
"""Evaluate a call to a built-in constant expression function.

Reference: https://openqasm.com/language/types.html#built-in-constant-expression-functions
"""
fn_name = expression.name.name
function, arity = FUNCTION_MAP[fn_name]
if len(expression.arguments) != arity:
raise_qasm3_error(
f"Function '{fn_name}' expects {arity} argument(s), but "
f"{len(expression.arguments)} were given",
err_type=FunctionCallError,
error_node=expression,
span=expression.span,
)
values = [
cls.evaluate_expression(argument, const_expr, reqd_type)[0]
for argument in expression.arguments
]
if fn_name in BIT_ROTATION_FUNCTIONS:
values[0] = cls._as_bit_value(values[0], expression)
try:
return function(*values)
except (TypeError, ValueError) as err:
raise_qasm3_error(
f"Invalid argument for function '{fn_name}': {err}",
err_type=FunctionCallError,
error_node=expression,
span=expression.span,
raised_from=err,
)

@classmethod
# pylint: disable-next=too-many-return-statements,too-many-branches,too-many-statements,too-many-locals,too-many-arguments
def evaluate_expression( # type: ignore[return]
Expand Down Expand Up @@ -519,11 +584,9 @@ def _get_external_function_return_type(expression):
return (None, statements)

if expression.name.name in FUNCTION_MAP:
_val, _ = cls.evaluate_expression(
expression.arguments[0], const_expr, reqd_type, validate_only
return _check_and_return_value(
cls._evaluate_builtin_function(expression, const_expr, reqd_type)
)
_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)
Expand Down
63 changes: 50 additions & 13 deletions src/pyqasm/maps/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

"""

from typing import Callable
from typing import Any, Callable

import numpy as np
from openqasm3.ast import (
Expand Down Expand Up @@ -243,16 +243,53 @@ def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value):
"s": {"ns": 1_000_000_000, "s": 1},
}

# Function map for complex functions
FUNCTION_MAP = {
"abs": np.abs,
"real": lambda v: v.real if isinstance(v, complex) else v,
"imag": lambda v: v.imag if isinstance(v, complex) else v,
"sqrt": np.sqrt,
"sin": np.sin,
"cos": np.cos,
"tan": np.tan,
"arccos": np.arccos,
"arcsin": np.arcsin,
"arctan": np.arctan,

def _popcount(value: Any) -> int:
"""Count the set bits of a ``bit[n]`` / ``uint[n]`` value."""
if isinstance(value, str):
value = int(value, 2) if value else 0
if not isinstance(value, (int, np.integer)) or value < 0:
raise TypeError("expected a non-negative 'bit[n]' or 'uint[n]' operand")
return int(value).bit_count()


def _rotl(value: BitValue, amount: Any) -> BitValue:
"""Rotate ``value`` left by ``amount`` bits, preserving its width."""
if not isinstance(amount, (int, np.integer)):
# Reject a non-integral rotation amount rather than silently truncating it.
raise TypeError("rotation amount must be an integer")
width = value.width
if width == 0:
return value
shift = int(amount) % width
return BitValue((int(value) << shift) | (int(value) >> (width - shift)), width)


# Functions whose first operand must carry a register width; see ``rotl`` / ``rotr``.
BIT_ROTATION_FUNCTIONS = frozenset({"rotl", "rotr"})

# Built-in constant expression functions, mapped to their implementation and arity.
# Reference: https://openqasm.com/language/types.html#built-in-constant-expression-functions
# ``pow`` is absent by design: it is ambiguous with the gate modifier of the same name,
# so ``openqasm3`` cannot parse ``pow(a, b)`` as a call. Upstream removed it from the
# spec (openqasm/openqasm#635); use the ``**`` operator instead.
FUNCTION_MAP: dict[str, tuple[Callable[..., Any], int]] = {
"abs": (np.abs, 1),
"real": (lambda v: v.real if isinstance(v, complex) else v, 1),
"imag": (lambda v: v.imag if isinstance(v, complex) else v, 1),
"sqrt": (np.sqrt, 1),
"sin": (np.sin, 1),
"cos": (np.cos, 1),
"tan": (np.tan, 1),
"arccos": (np.arccos, 1),
"arcsin": (np.arcsin, 1),
"arctan": (np.arctan, 1),
"exp": (np.exp, 1),
"log": (np.log, 1),
"ceiling": (np.ceil, 1),
"floor": (np.floor, 1),
"mod": (np.mod, 2),
"popcount": (_popcount, 1),
"rotl": (_rotl, 2),
"rotr": (lambda v, n: _rotl(v, -n), 2),
}
26 changes: 20 additions & 6 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from pyqasm.exceptions import (
BreakSignal,
ContinueSignal,
FunctionCallError,
LoopControlSignal,
LoopLimitExceededError,
ValidationError,
Expand Down Expand Up @@ -609,13 +610,14 @@ def _qubit_register_consolidation(
return _valid_statements

def _handle_function_init_expression(
self, expression: qasm3_ast.FunctionCall, init_value: Any
self, expression: qasm3_ast.FunctionCall, init_value: Any, base_type: Any = None
) -> None | qasm3_ast.Expression:
"""Handle function initialization expression.

Args:
expression (FunctionCall): The statement to handle function initialization expression.
init_value (Any): The value to handle function initialization expression.
base_type (Any): The declared type of the assignment target, if known.

Returns:
None | Expression: The resultant expression if
Expand All @@ -624,8 +626,15 @@ def _handle_function_init_expression(
if isinstance(expression, qasm3_ast.FunctionCall):
func_name = expression.name.name
if func_name in FUNCTION_MAP:
if isinstance(init_value, (float, int)):
return qasm3_ast.FloatLiteral(init_value)
# ``BitValue`` is an ``int`` subclass, so it must be matched first.
if isinstance(init_value, BitValue):
if isinstance(base_type, qasm3_ast.BitType):
return qasm3_ast.BitstringLiteral(int(init_value), init_value.width)
return qasm3_ast.IntegerLiteral(int(init_value))
if isinstance(init_value, (int, np.integer)):
return qasm3_ast.IntegerLiteral(int(init_value))
if isinstance(init_value, (float, np.floating)):
return qasm3_ast.FloatLiteral(float(init_value))
return None

def _handle_extern_function_cleanup(
Expand Down Expand Up @@ -1891,7 +1900,9 @@ def _visit_constant_declaration(

if isinstance(statement.init_expression, qasm3_ast.FunctionCall):
statement.init_expression = (
self._handle_function_init_expression(statement.init_expression, init_value)
self._handle_function_init_expression(
statement.init_expression, init_value, base_type
)
or statement.init_expression
)
self._handle_extern_function_cleanup(statements, statement)
Expand Down Expand Up @@ -2136,7 +2147,9 @@ def _visit_classical_declaration(

if isinstance(statement.init_expression, qasm3_ast.FunctionCall):
statement.init_expression = (
self._handle_function_init_expression(statement.init_expression, init_value)
self._handle_function_init_expression(
statement.init_expression, init_value, base_type
)
or statement.init_expression
)

Expand Down Expand Up @@ -2321,7 +2334,7 @@ def _visit_classical_assignment(

if isinstance(statement.rvalue, qasm3_ast.FunctionCall):
statement.rvalue = (
self._handle_function_init_expression(statement.rvalue, rvalue_eval)
self._handle_function_init_expression(statement.rvalue, rvalue_eval, lvar_base_type)
or statement.rvalue
)

Expand Down Expand Up @@ -2675,6 +2688,7 @@ def _visit_function_call(
return None, []
raise_qasm3_error(
f"Undefined subroutine '{fn_name}' was called",
err_type=FunctionCallError,
error_node=statement,
span=statement.span,
)
Expand Down
Loading