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
43 changes: 36 additions & 7 deletions src/pyqasm/pulse/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,40 @@ def _visit_function_call( # pylint: disable=too-many-branches, too-many-stateme

return _return_value, [statement]

def _visit_expression_statement(
self, statement: qasm3_ast.ExpressionStatement
) -> list[qasm3_ast.Statement]:
"""Visit an expression statement in an OpenPulse block.

OpenPulse functions retain their specialized validation and output.
Other expressions use the main visitor's evaluator and discard their
value.

Args:
statement (ExpressionStatement): The expression statement to visit.

Returns:
list[Statement]: Statements produced while evaluating the expression.
"""
expression = statement.expression
pulse_functions = {
*OPENPULSE_FRAME_FUNCTION_MAP,
*OPENPULSE_WAVEFORM_FUNCTION_MAP,
*OPENPULSE_CAPTURE_FUNCTION_MAP,
"get_phase",
"get_frequency",
"newframe",
"play",
}
if (
isinstance(expression, qasm3_ast.FunctionCall)
and expression.name.name in pulse_functions
):
_, statements = self._visit_function_call(expression)
return statements # type: ignore[return-value]
_, statements = Qasm3ExprEvaluator.evaluate_expression(expression)
return statements

def visit_statement(
self, statement: qasm3_ast.Statement | qasm3_ast.Pragma
) -> list[qasm3_ast.Statement]:
Expand All @@ -789,7 +823,7 @@ def visit_statement(
visit_map = {
qasm3_ast.QuantumBarrier: self._visit_barrier,
qasm3_ast.ClassicalDeclaration: self._visit_classical_declaration,
qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression),
qasm3_ast.ExpressionStatement: self._visit_expression_statement,
qasm3_ast.DelayInstruction: self._qasm_visitor._visit_delay_statement,
qasm3_ast.ClassicalAssignment: self._visit_classical_assignment,
qasm3_ast.ConstantDeclaration: self._visit_classical_declaration,
Expand All @@ -799,12 +833,7 @@ def visit_statement(
visitor_function = visit_map.get(type(statement))

if visitor_function:
if isinstance(statement, qasm3_ast.ExpressionStatement):
# these return a tuple of return value and list of statements
_, ret_stmts = visitor_function(statement) # type: ignore[operator]
result.extend(ret_stmts)
else:
result.extend(visitor_function(statement)) # type: ignore[operator]
result.extend(visitor_function(statement)) # type: ignore[operator]
else:
if isinstance(statement, qasm3_ast.ReturnStatement):
if statement.expression:
Expand Down
36 changes: 29 additions & 7 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def _construct_visit_map(self):
qasm3_ast.SwitchStatement: self._visit_switch_statement,
qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition,
qasm3_ast.ExternDeclaration: self._visit_subroutine_definition,
qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression),
qasm3_ast.ExpressionStatement: self._visit_expression_statement,
qasm3_ast.IODeclaration: lambda x: [],
qasm3_ast.BreakStatement: self._visit_break,
qasm3_ast.ContinueStatement: self._visit_continue,
Expand Down Expand Up @@ -1637,6 +1637,15 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches, too-man
)
return stmts # type: ignore

if (
isinstance(operation, qasm3_ast.QuantumGate)
and operation.name.name not in self._custom_gates
and not self._is_black_box_gate(operation.name.name)
):
# Resolve the operation before its operands so an unknown gate is
# reported even when one of its qubits is also undeclared.
map_qasm_op_to_callable(operation)

self._in_generic_gate_op_scope += 1

# only needs to be done once for a gate operation
Expand Down Expand Up @@ -3505,6 +3514,24 @@ def _visit_include(self, include: qasm3_ast.Include) -> list[qasm3_ast.Statement

return [include]

@staticmethod
def _visit_expression_statement(
statement: qasm3_ast.ExpressionStatement,
) -> list[qasm3_ast.Statement]:
"""Evaluate an expression statement and discard its value.

Statements produced while evaluating the expression are retained so
that calls to user-defined and external functions keep their effects.

Args:
statement (ExpressionStatement): The expression statement to visit.

Returns:
list[Statement]: Statements produced while evaluating the expression.
"""
_, statements = Qasm3ExprEvaluator.evaluate_expression(statement.expression)
return statements

def visit_statement(
self, statement: qasm3_ast.Statement | qasm3_ast.Pragma
) -> list[qasm3_ast.Statement]:
Expand All @@ -3527,12 +3554,7 @@ def visit_statement(

visitor_function = self._visit_map.get(type(statement))
if visitor_function:
if isinstance(statement, qasm3_ast.ExpressionStatement):
# these return a tuple of return value and list of statements
_, ret_stmts = visitor_function(statement) # type: ignore[operator]
result.extend(ret_stmts)
else:
result.extend(visitor_function(statement)) # type: ignore[operator]
result.extend(visitor_function(statement)) # type: ignore[operator]
else:
raise_qasm3_error(
f"Unsupported statement of type {type(statement)}",
Expand Down
35 changes: 35 additions & 0 deletions tests/qasm3/openpulse/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,47 @@

"""

import openqasm3.ast as qasm3_ast
import pytest

from pyqasm.entrypoint import loads
from pyqasm.exceptions import ValidationError


@pytest.mark.parametrize("expression", ["1 + 2;", "i;", "sin(1.0);"])
def test_pure_expression_statements_are_discarded(expression):
"""Pure expressions in calibration blocks do not emit statements."""
module = loads(f"""
OPENQASM 3.0;
defcalgrammar "openpulse";
cal {{
int i = 1;
{expression}
}}
""")

module.validate()
module.unroll()

calibration = next(
statement
for statement in module.unrolled_ast.statements
if isinstance(statement, qasm3_ast.CalibrationStatement)
)
assert [line.strip() for line in calibration.body.splitlines() if line.strip()] == [
"int i = 1;"
]


@pytest.mark.parametrize("operation", ["validate", "unroll"])
def test_unknown_expression_statement_call_raises_validation_error(operation):
"""Unknown calls in calibration blocks use the public error type."""
module = loads('OPENQASM 3.0; defcalgrammar "openpulse"; cal { unknown(); }')

with pytest.raises(ValidationError, match="Undefined subroutine 'unknown'"):
getattr(module, operation)()


@pytest.mark.parametrize(
"qasm_code,error_message,error_span",
[
Expand Down
2 changes: 1 addition & 1 deletion tests/qasm3/resources/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def test_fixture():
"Unsupported / undeclared QASM operation: custom_gate",
6,
8,
"custom_gate q1[0], q1[1];", # expanded line
"custom_gate q1;",
),
"parameter_mismatch_1": (
"""
Expand Down
84 changes: 84 additions & 0 deletions tests/qasm3/test_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

"""

import openqasm3.ast as qasm3_ast
import pytest

from pyqasm.entrypoint import loads
Expand Down Expand Up @@ -105,3 +106,86 @@ def test_incorrect_expressions(caplog):
loads("OPENQASM 3; qubit q; int x; rx(x) q;").validate()
assert "Error at line 1" in caplog.text
assert "x" in caplog.text


@pytest.mark.parametrize(
"expression",
[
"1;",
"value;",
"value + 2;",
"-value;",
"values[0];",
"sin(1.0);",
],
)
def test_expression_statements_are_evaluated_and_discarded(expression):
"""Pure expression statements are valid but do not emit operations."""
module = loads(f"""
OPENQASM 3.0;
include "stdgates.inc";
int value = 1;
array[int[32], 2] values = {{1, 2}};
qubit q;
{expression}
x q;
""")

module.validate()
module.unroll()

assert not any(
isinstance(statement, qasm3_ast.ExpressionStatement)
for statement in module.unrolled_ast.statements
)
check_single_qubit_gate_op(module.unrolled_ast, 1, [0], "x")


def test_subroutine_expression_statement_retains_operations():
"""Statements produced by evaluating a subroutine call are retained."""
module = loads("""
OPENQASM 3.0;
include "stdgates.inc";
def apply_x(qubit target) {
x target;
}
qubit q;
apply_x(q);
""")

module.validate()
module.unroll()

check_single_qubit_gate_op(module.unrolled_ast, 1, [0], "x")


@pytest.mark.parametrize("operation", ["validate", "unroll"])
def test_unknown_expression_statement_call_raises_validation_error(operation):
"""Unknown calls in expression statements use the public error type."""
module = loads("OPENQASM 3.0; unknown();")

with pytest.raises(ValidationError, match="Undefined subroutine 'unknown'"):
getattr(module, operation)()


@pytest.mark.parametrize(
"source,error",
[
("OPENQASM 3.0; unknown;", "Undefined identifier 'unknown'"),
(
"OPENQASM 3.0; unknown missing_qubit;",
"Unsupported / undeclared QASM operation: unknown",
),
(
"OPENQASM 3.0; qubit q; unknown q;",
"Unsupported / undeclared QASM operation: unknown",
),
],
)
@pytest.mark.parametrize("operation", ["validate", "unroll"])
def test_unknown_gate_reports_its_name_before_checking_operands(source, error, operation):
"""Unknown gate names are reported even when an operand is undeclared."""
module = loads(source)

with pytest.raises(ValidationError, match=error):
getattr(module, operation)()
Loading