Reference OpenQASM AST in Python - contains the reference abstract syntax tree (AST) for representing OpenQASM 3 programs, tools to parse text into this AST, and tools to manipulate the AST
—
Visitor and transformer patterns for systematic AST traversal, analysis, and modification with optional context support. These classes enable powerful AST manipulation patterns including tree walking, analysis, transformation, and code generation.
Generic visitor pattern for AST traversal with flexible context support.
class QASMVisitor(Generic[T]):
"""
A node visitor base class that walks the abstract syntax tree and calls a
visitor function for every node found. This function may return a value
which is forwarded by the `visit` method.
The optional context argument allows the visitor to hold temporary state
while visiting nodes without modifying the AST or the visitor instance.
"""
def visit(self, node: QASMNode, context: Optional[T] = None):
"""
Visit a node and dispatch to the appropriate visitor method.
Args:
node: AST node to visit
context: Optional context object for holding temporary state
Returns:
Result from the visitor method (if any)
"""
def generic_visit(self, node: QASMNode, context: Optional[T] = None):
"""
Called if no explicit visitor function exists for a node.
This method recursively visits all child nodes in the AST.
Override this method to change the default traversal behavior.
Args:
node: AST node to visit
context: Optional context object
Returns:
None by default, but can be overridden
"""Visitor subclass that enables modification of AST nodes during traversal.
class QASMTransformer(QASMVisitor[T]):
"""
A QASMVisitor subclass that allows modification of AST nodes during traversal.
This class enables tree transformation operations including node replacement,
deletion, and list modifications. It handles the complexities of maintaining
valid AST structure during transformations.
"""
def generic_visit(self, node: QASMNode, context: Optional[T] = None) -> QASMNode:
"""
Visit and potentially modify nodes and their children.
This method visits all child nodes and can replace them with modified
versions. It handles lists, single nodes, and None values appropriately.
Args:
node: AST node to visit and potentially transform
context: Optional context object
Returns:
The node (potentially modified) or a replacement node
"""from openqasm3 import ast, visitor
import openqasm3
class GateAnalyzer(visitor.QASMVisitor):
"""Analyze quantum gates in a program"""
def __init__(self):
self.gate_calls = []
self.gate_definitions = []
self.total_qubits = 0
def visit_QuantumGate(self, node):
"""Visit quantum gate calls"""
self.gate_calls.append({
'name': node.name.name,
'qubits': len(node.qubits),
'parameters': len(node.arguments)
})
self.generic_visit(node)
def visit_QuantumGateDefinition(self, node):
"""Visit quantum gate definitions"""
self.gate_definitions.append({
'name': node.name.name,
'qubits': len(node.qubits),
'parameters': len(node.arguments)
})
self.generic_visit(node)
def visit_QubitDeclaration(self, node):
"""Count total qubits declared"""
if node.size:
# Array declaration like qubit[4] q
if isinstance(node.size, ast.IntegerLiteral):
self.total_qubits += node.size.value
else:
# Single qubit declaration like qubit q
self.total_qubits += 1
self.generic_visit(node)
# Use the analyzer
qasm_source = '''
OPENQASM 3.0;
qubit[3] q;
gate bell q0, q1 {
h q0;
cx q0, q1;
}
bell q[0], q[1];
h q[2];
'''
program = openqasm3.parse(qasm_source)
analyzer = GateAnalyzer()
analyzer.visit(program)
print(f"Total qubits: {analyzer.total_qubits}")
print(f"Gate definitions: {analyzer.gate_definitions}")
print(f"Gate calls: {analyzer.gate_calls}")from openqasm3 import ast, visitor
import openqasm3
class GateInverter(visitor.QASMTransformer):
"""Transform gates to add inverse modifiers"""
def visit_QuantumGate(self, node):
"""Add inverse modifier to all gates"""
# Create inverse modifier
inv_modifier = ast.QuantumGateModifier(
modifier=ast.GateModifierName.inv,
argument=None
)
# Create new gate with inverse modifier
new_gate = ast.QuantumGate(
modifiers=[inv_modifier] + node.modifiers,
name=node.name,
arguments=node.arguments,
qubits=node.qubits,
duration=node.duration
)
return self.generic_visit(new_gate)
# Transform a program
program = openqasm3.parse('''
OPENQASM 3.0;
qubit[2] q;
h q[0];
cx q[0], q[1];
''')
transformer = GateInverter()
transformed_program = transformer.visit(program)
# Convert back to text to see the result
result = openqasm3.dumps(transformed_program)
print(result)from openqasm3 import ast, visitor
import openqasm3
from typing import List, Set
class ScopeInfo:
"""Context object to track scope information"""
def __init__(self):
self.declared_variables: Set[str] = set()
self.used_variables: Set[str] = set()
self.scope_level: int = 0
class VariableAnalyzer(visitor.QASMVisitor[ScopeInfo]):
"""Analyze variable usage with scope tracking"""
def visit_Program(self, node, context=None):
"""Start analysis with global scope"""
if context is None:
context = ScopeInfo()
self.generic_visit(node, context)
# Report unused variables
unused = context.declared_variables - context.used_variables
if unused:
print(f"Unused variables: {unused}")
def visit_ClassicalDeclaration(self, node, context):
"""Track variable declarations"""
var_name = node.identifier.name
context.declared_variables.add(var_name)
print(f"Declared variable '{var_name}' at scope level {context.scope_level}")
# Visit initialization expression
if node.init_expression:
self.visit(node.init_expression, context)
def visit_Identifier(self, node, context):
"""Track variable usage"""
var_name = node.name
if var_name in context.declared_variables:
context.used_variables.add(var_name)
self.generic_visit(node, context)
def visit_SubroutineDefinition(self, node, context):
"""Enter new scope for subroutine"""
# Create new scope context
new_context = ScopeInfo()
new_context.declared_variables = context.declared_variables.copy()
new_context.used_variables = context.used_variables.copy()
new_context.scope_level = context.scope_level + 1
# Add subroutine parameters to scope
for arg in node.arguments:
if isinstance(arg, ast.ClassicalArgument):
new_context.declared_variables.add(arg.name.name)
# Visit subroutine body with new context
for stmt in node.body:
self.visit(stmt, new_context)
# Merge used variables back to parent context
context.used_variables.update(new_context.used_variables)
# Analyze a program with variable usage
program = openqasm3.parse('''
OPENQASM 3.0;
int[32] x = 42;
int[32] y = 10;
int[32] result;
def int[32] add_numbers(int[32] a, int[32] b) {
return a + b;
}
result = add_numbers(x, y);
''')
analyzer = VariableAnalyzer()
analyzer.visit(program)from openqasm3 import ast, visitor
import openqasm3
class QuantumCircuitExtractor(visitor.QASMVisitor):
"""Extract quantum circuit information"""
def __init__(self):
self.circuit_info = {
'qubits': {},
'gates': [],
'measurements': []
}
def visit_QubitDeclaration(self, node):
"""Extract qubit information"""
qubit_name = node.qubit.name
if node.size:
if isinstance(node.size, ast.IntegerLiteral):
size = node.size.value
else:
size = f"expr({node.size})"
else:
size = 1
self.circuit_info['qubits'][qubit_name] = size
self.generic_visit(node)
def visit_QuantumGate(self, node):
"""Extract gate operations"""
gate_info = {
'name': node.name.name,
'qubits': [],
'parameters': [],
'modifiers': []
}
# Extract qubit targets
for qubit in node.qubits:
if isinstance(qubit, ast.Identifier):
gate_info['qubits'].append(qubit.name)
elif isinstance(qubit, ast.IndexedIdentifier):
qubit_ref = f"{qubit.name.name}[{qubit.indices}]"
gate_info['qubits'].append(qubit_ref)
# Extract parameters
for param in node.arguments:
if isinstance(param, ast.Identifier):
gate_info['parameters'].append(param.name)
elif isinstance(param, ast.FloatLiteral):
gate_info['parameters'].append(param.value)
elif isinstance(param, ast.IntegerLiteral):
gate_info['parameters'].append(param.value)
# Extract modifiers
for modifier in node.modifiers:
mod_info = {'type': modifier.modifier.name}
if modifier.argument:
mod_info['argument'] = str(modifier.argument)
gate_info['modifiers'].append(mod_info)
self.circuit_info['gates'].append(gate_info)
self.generic_visit(node)
def visit_QuantumMeasurementStatement(self, node):
"""Extract measurement operations"""
measurement_info = {
'qubit': str(node.measure.qubit),
'target': str(node.target) if node.target else None
}
self.circuit_info['measurements'].append(measurement_info)
self.generic_visit(node)
# Extract circuit information
program = openqasm3.parse('''
OPENQASM 3.0;
qubit[3] q;
bit[3] c;
h q[0];
cx q[0], q[1];
ry(pi/4) q[2];
measure q[0] -> c[0];
measure q[1] -> c[1];
''')
extractor = QuantumCircuitExtractor()
extractor.visit(program)
import json
print(json.dumps(extractor.circuit_info, indent=2))from openqasm3 import ast, visitor
import openqasm3
class SafeGateReplacer(visitor.QASMTransformer):
"""Safely replace gate names with error handling"""
def __init__(self, replacement_map):
self.replacement_map = replacement_map
self.warnings = []
def visit_QuantumGate(self, node):
"""Replace gate names according to mapping"""
original_name = node.name.name
if original_name in self.replacement_map:
new_name = self.replacement_map[original_name]
# Create new gate with replaced name
new_gate = ast.QuantumGate(
modifiers=node.modifiers,
name=ast.Identifier(new_name),
arguments=node.arguments,
qubits=node.qubits,
duration=node.duration
)
print(f"Replaced gate '{original_name}' with '{new_name}'")
return self.generic_visit(new_gate)
else:
# Gate not in replacement map
self.warnings.append(f"Gate '{original_name}' not found in replacement map")
return self.generic_visit(node)
# Use the transformer
replacements = {
'h': 'hadamard',
'cx': 'cnot',
'x': 'pauli_x'
}
program = openqasm3.parse('''
OPENQASM 3.0;
qubit[2] q;
h q[0];
cx q[0], q[1];
y q[1]; // This gate won't be replaced
''')
replacer = SafeGateReplacer(replacements)
transformed = replacer.visit(program)
print("\nWarnings:")
for warning in replacer.warnings:
print(f" {warning}")
print("\nTransformed program:")
print(openqasm3.dumps(transformed))from openqasm3 import visitor
class CompositeAnalyzer(visitor.QASMVisitor):
"""Combine multiple analysis passes"""
def __init__(self):
self.analyzers = [
GateAnalyzer(),
VariableAnalyzer(),
QuantumCircuitExtractor()
]
def visit(self, node, context=None):
# Run all analyzers
for analyzer in self.analyzers:
analyzer.visit(node, context)
# Run default visit
return super().visit(node, context)class StatefulVisitor(visitor.QASMVisitor):
"""Visitor that maintains complex state"""
def __init__(self):
self.state_stack = []
self.current_function = None
self.call_graph = {}
def _push_state(self, state_name):
self.state_stack.append(state_name)
def _pop_state(self):
if self.state_stack:
return self.state_stack.pop()
return None
def visit_SubroutineDefinition(self, node):
self._push_state('subroutine')
old_function = self.current_function
self.current_function = node.name.name
self.generic_visit(node)
self.current_function = old_function
self._pop_state()The visitor pattern provides a powerful and flexible way to work with OpenQASM 3 ASTs, enabling everything from simple analysis to complex transformations while maintaining clean, readable code.
Install with Tessl CLI
npx tessl i tessl/pypi-openqasm3