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
—
Expression precedence handling, specification version metadata, and other utility functions for working with OpenQASM 3 ASTs. These utilities provide essential support for expression parsing, language versioning, and AST manipulation.
Functions and constants for handling operator precedence in expressions to ensure proper parenthesization and evaluation order.
def precedence(node_type: type) -> int:
"""
Get the precedence level for an expression node type.
Higher numbers indicate higher precedence (tighter binding).
Used by the printer to determine when parentheses are needed
around subexpressions.
Args:
node_type: AST node class (e.g., ast.BinaryExpression, ast.FunctionCall)
Returns:
Integer precedence level (0-14, where 14 is highest precedence)
Examples:
precedence(ast.FunctionCall) -> 13 # High precedence
precedence(ast.BinaryExpression) -> varies by operator
precedence(ast.Identifier) -> 14 # Highest precedence
"""Constants and information about supported OpenQASM 3 specification versions.
supported_versions: List[str]
"""
A list of specification versions supported by this package.
Each version is a string, e.g., '3.0', '3.1'.
Current supported versions: ["3.0", "3.1"]
This list indicates which OpenQASM 3 language specification
versions are fully supported by the AST and parser.
"""Internal precedence table mapping expression types to precedence levels.
# Expression precedence levels (from lowest to highest):
# 0 - Concatenation (++)
# 1 - Logical OR (||)
# 2 - Logical AND (&&)
# 3 - Bitwise OR (|)
# 4 - Bitwise XOR (^)
# 5 - Bitwise AND (&)
# 6 - Equality (==, !=)
# 7 - Comparison (<, <=, >, >=)
# 8 - Bit shifts (<<, >>)
# 9 - Addition/Subtraction (+, -)
# 10 - Multiplication/Division/Modulo (*, /, %)
# 11 - Power (**), Unary operators (~, !, -)
# 12 - Reserved for future use
# 13 - Function-like operations (function calls, indexing, casting, durationof)
# 14 - Literals and identifiers (highest precedence)from openqasm3 import ast, properties
import openqasm3
# Understanding precedence levels
print(f"Identifier precedence: {properties.precedence(ast.Identifier)}")
print(f"Function call precedence: {properties.precedence(ast.FunctionCall)}")
print(f"Binary expression precedence: {properties.precedence(ast.BinaryExpression)}")
print(f"Unary expression precedence: {properties.precedence(ast.UnaryExpression)}")
# Create expressions that demonstrate precedence
# a + b * c should be parsed as a + (b * c)
expr1 = ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.Identifier("a"),
rhs=ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.Identifier("b"),
rhs=ast.Identifier("c")
)
)
# When printed, this will not need parentheses around b * c
# because multiplication has higher precedence than addition
code1 = openqasm3.dumps(expr1)
print(f"Expression 1: {code1}") # Output: a + b * c
# But (a + b) * c needs parentheses
expr2 = ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.Identifier("a"),
rhs=ast.Identifier("b")
),
rhs=ast.Identifier("c")
)
code2 = openqasm3.dumps(expr2)
print(f"Expression 2: {code2}") # Output: (a + b) * cfrom openqasm3 import spec
import openqasm3
def check_version_support(version_string):
"""Check if a version is supported by this package"""
if version_string in spec.supported_versions:
print(f"Version {version_string} is supported")
return True
else:
print(f"Version {version_string} is not supported")
print(f"Supported versions: {spec.supported_versions}")
return False
# Check various versions
check_version_support("3.0") # True
check_version_support("3.1") # True
check_version_support("2.0") # False
check_version_support("3.2") # False
# Parse programs with version checking
def safe_parse_with_version_check(qasm_source):
"""Parse with version validation"""
try:
program = openqasm3.parse(qasm_source)
if program.version:
if program.version in spec.supported_versions:
print(f"Successfully parsed OpenQASM {program.version} program")
return program
else:
print(f"Warning: Program uses unsupported version {program.version}")
print(f"Supported versions: {spec.supported_versions}")
return program
else:
print("Program has no version declaration")
return program
except Exception as e:
print(f"Parsing failed: {e}")
return None
# Test with different versions
program1 = safe_parse_with_version_check("OPENQASM 3.0;\nqubit q;")
program2 = safe_parse_with_version_check("OPENQASM 3.1;\nqubit q;")
program3 = safe_parse_with_version_check("OPENQASM 2.0;\nqubit q;")from openqasm3 import ast, properties
import openqasm3
class ExpressionAnalyzer:
"""Analyze expression complexity based on precedence"""
def __init__(self):
self.complexity_score = 0
self.operator_counts = {}
def analyze_expression(self, expr):
"""Analyze an expression's complexity"""
self.complexity_score = 0
self.operator_counts = {}
self._visit_expression(expr)
return {
'complexity': self.complexity_score,
'operators': self.operator_counts
}
def _visit_expression(self, expr):
"""Recursively analyze expression nodes"""
expr_type = type(expr)
precedence_level = properties.precedence(expr_type)
# Higher precedence (tighter binding) contributes less to complexity
complexity_contribution = max(1, 15 - precedence_level)
self.complexity_score += complexity_contribution
if isinstance(expr, ast.BinaryExpression):
op_name = expr.op.name
self.operator_counts[op_name] = self.operator_counts.get(op_name, 0) + 1
self._visit_expression(expr.lhs)
self._visit_expression(expr.rhs)
elif isinstance(expr, ast.UnaryExpression):
op_name = f"unary_{expr.op.name}"
self.operator_counts[op_name] = self.operator_counts.get(op_name, 0) + 1
self._visit_expression(expr.expression)
elif isinstance(expr, ast.FunctionCall):
self.operator_counts['function_call'] = self.operator_counts.get('function_call', 0) + 1
for arg in expr.arguments:
self._visit_expression(arg)
elif isinstance(expr, ast.IndexExpression):
self.operator_counts['indexing'] = self.operator_counts.get('indexing', 0) + 1
self._visit_expression(expr.collection)
# Test the analyzer
analyzer = ExpressionAnalyzer()
# Simple expression: a + b
simple_expr = ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.Identifier("a"),
rhs=ast.Identifier("b")
)
result1 = analyzer.analyze_expression(simple_expr)
print(f"Simple expression complexity: {result1}")
# Complex expression: sin(a * b) + sqrt(c ** 2 + d ** 2)
complex_expr = ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.FunctionCall(
name=ast.Identifier("sin"),
arguments=[ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.Identifier("a"),
rhs=ast.Identifier("b")
)]
),
rhs=ast.FunctionCall(
name=ast.Identifier("sqrt"),
arguments=[ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.BinaryExpression(
op=ast.BinaryOperator["**"],
lhs=ast.Identifier("c"),
rhs=ast.IntegerLiteral(2)
),
rhs=ast.BinaryExpression(
op=ast.BinaryOperator["**"],
lhs=ast.Identifier("d"),
rhs=ast.IntegerLiteral(2)
)
)]
)
)
result2 = analyzer.analyze_expression(complex_expr)
print(f"Complex expression complexity: {result2}")from openqasm3 import ast, properties
import openqasm3
class ExpressionBuilder:
"""Build expressions with automatic precedence handling"""
def needs_parentheses(self, parent_op, child_expr, is_right_operand=False):
"""Determine if child expression needs parentheses"""
if not isinstance(child_expr, ast.BinaryExpression):
return False
parent_prec = self._get_binary_precedence(parent_op)
child_prec = self._get_binary_precedence(child_expr.op)
# Lower precedence always needs parentheses
if child_prec < parent_prec:
return True
# Same precedence on right side of non-associative operators
if child_prec == parent_prec and is_right_operand:
# Right-associative operators like ** don't need parentheses
if parent_op == ast.BinaryOperator["**"]:
return False
# Left-associative operators need parentheses on right
return True
return False
def _get_binary_precedence(self, op):
"""Get precedence for binary operators"""
precedence_map = {
ast.BinaryOperator["||"]: 1,
ast.BinaryOperator["&&"]: 2,
ast.BinaryOperator["|"]: 3,
ast.BinaryOperator["^"]: 4,
ast.BinaryOperator["&"]: 5,
ast.BinaryOperator["=="]: 6,
ast.BinaryOperator["!="]: 6,
ast.BinaryOperator["<"]: 7,
ast.BinaryOperator["<="]: 7,
ast.BinaryOperator[">"]: 7,
ast.BinaryOperator[">="]: 7,
ast.BinaryOperator["<<"]: 8,
ast.BinaryOperator[">>"]: 8,
ast.BinaryOperator["+"]: 9,
ast.BinaryOperator["-"]: 9,
ast.BinaryOperator["*"]: 10,
ast.BinaryOperator["/"]: 10,
ast.BinaryOperator["%"]: 10,
ast.BinaryOperator["**"]: 11,
}
return precedence_map.get(op, 0)
def build_expression_chain(self, *terms_and_ops):
"""Build a chain of binary expressions with proper precedence"""
if len(terms_and_ops) < 3:
return terms_and_ops[0] if terms_and_ops else None
# Parse alternating terms and operators
terms = terms_and_ops[::2]
ops = terms_and_ops[1::2]
# Build expression tree respecting precedence
result = terms[0]
for i, op in enumerate(ops):
right_term = terms[i + 1]
result = ast.BinaryExpression(
op=op,
lhs=result,
rhs=right_term
)
return result
# Example usage
builder = ExpressionBuilder()
# Build: a + b * c - d
expr = builder.build_expression_chain(
ast.Identifier("a"),
ast.BinaryOperator["+"],
ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.Identifier("b"),
rhs=ast.Identifier("c")
),
ast.BinaryOperator["-"],
ast.Identifier("d")
)
code = openqasm3.dumps(expr)
print(f"Built expression: {code}")
# Test parentheses detection
test_expr = ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.Identifier("x"),
rhs=ast.Identifier("y")
)
needs_parens = builder.needs_parentheses(
ast.BinaryOperator["+"],
test_expr,
is_right_operand=True
)
print(f"x * y needs parentheses in (? + x * y): {needs_parens}")from openqasm3 import ast, properties
import openqasm3
def simplify_expression(expr):
"""Simplify expressions using precedence rules"""
if isinstance(expr, ast.BinaryExpression):
# Simplify operands first
left = simplify_expression(expr.lhs)
right = simplify_expression(expr.rhs)
# Apply simplification rules
if expr.op == ast.BinaryOperator["+"]:
# 0 + x = x, x + 0 = x
if isinstance(left, ast.IntegerLiteral) and left.value == 0:
return right
if isinstance(right, ast.IntegerLiteral) and right.value == 0:
return left
elif expr.op == ast.BinaryOperator["*"]:
# 1 * x = x, x * 1 = x
if isinstance(left, ast.IntegerLiteral) and left.value == 1:
return right
if isinstance(right, ast.IntegerLiteral) and right.value == 1:
return left
# 0 * x = 0, x * 0 = 0
if isinstance(left, ast.IntegerLiteral) and left.value == 0:
return ast.IntegerLiteral(0)
if isinstance(right, ast.IntegerLiteral) and right.value == 0:
return ast.IntegerLiteral(0)
# Return with simplified operands
return ast.BinaryExpression(op=expr.op, lhs=left, rhs=right)
return expr
# Test simplification
original = ast.BinaryExpression(
op=ast.BinaryOperator["+"],
lhs=ast.BinaryExpression(
op=ast.BinaryOperator["*"],
lhs=ast.Identifier("x"),
rhs=ast.IntegerLiteral(1)
),
rhs=ast.IntegerLiteral(0)
)
simplified = simplify_expression(original)
print(f"Original: {openqasm3.dumps(original)}")
print(f"Simplified: {openqasm3.dumps(simplified)}")The utilities module ensures compatibility with multiple OpenQASM 3 specification versions:
Future versions will be added to supported_versions as they are implemented and tested.
The utilities work closely with other package modules:
These utilities form the foundation for correct OpenQASM 3 language processing throughout the package.
Install with Tessl CLI
npx tessl i tessl/pypi-openqasm3