Înainte de a mă cufunda în lumea fantastică a compilatorului Cuda-Q, m-am gândit că ar merita să scriu o scurtă notă despre arta rescrierea AST.

Ne putem întreba: care este diferența dintre rescrierea AST și compilare? Pe scurt, fiecare compilare include rescrierea AST, dar nu fiecare rescriere AST se consideră compilare.
De asemenea, compilarea este procesul de coborâre a codului dintr-un limbaj în altul, în timp ce rescrierea AST va rămâne de obicei în cadrul aceluiași limbaj.
API-ul arborelui sintactic abstract Python (AST)
Link to heading
Python oferă o bibliotecă standard pentru lucrul cu AST-uri. Există aproximativ patru categorii de noduri într-un AST Python:
De exemplu, 10, "salut", Adevărat.
Constant(value=10)
Constant(value="hello")
Constant(value=True)
De exemplu x, y, z. Variabilele pot fi accesate fie în modul de citire (încărcare), fie în modul de scriere (stocare).
Name(id='x', ctx=Load())
Name(id='y', ctx=Store())
De exemplu, 1 + 2, x + 2, x > y. Expresiile sunt noduri AST care produc o valoare.
BinOp(left=Constant(value=1), op=Add(), right=Constant(value=2))
BinOp(left=Name("x", Load()),op=Add(),right=Constant(value=2))
Compare(left=Name("x", Load()),ops=[Gt()],comparators=[Name(id='y', ctx=Load())])
De exemplu, if, for, while, return, break, continue. Instrucțiunile sunt noduri AST care efectuează acțiuni și apar ca construcții de nivel superior sau de nivel de bloc în codul Python.
De exemplu, următorul cod:
este echivalent cu:
If(
test=Compare(left=Name("x", Load()),ops=[Gt()],comparators=[Constant(value=1)]),
body=[Pass()],
orelse=[]
)
Exemplu concret: Condiționale în nuclee cuantice
Link to heading
Să ne imaginăm că vrem să transformăm acest cod:
@kernel
def conditional_play(qubit: Qubit):
iq = qubit.ancilla.readout()
if iq.i > 0.5:
qubit.main.play("waveform")
în
def kernelized_conditional_play(qubit: Qubit):
iq = qubit.ancilla.readout()
with cc._if(iq.i > 0.5):
qubit.main.play('waveform')
Ideea este că iq este un Readout în timpul rulării care nu ar trebui evaluat de interpretorul phyton, ci de controlerul cuantic. Pentru ca acest lucru să funcționeze, trebuie să se creeze un decorator kernel:
class kernel:
def __init__(self, func):
self.func = func
# Uncompile wrapped function (convert it into a string)
source = self.decompile(func)
# Parse the string into an AST
tree = self.parse_to_ast(source)
# Transform the AST, converting the "if" into "with if_()"
tree = Transformer().visit(tree)
# Recompile the AST into a binary
binary = self.recompile(tree)
# Make a namespace for execution
namespace = func.__globals__.copy()
# This does not really executes the function, but rather creates
# a new function based on the modified AST
exec(binary, None, namespace)
# The new function is now available in the namespace
self.kernel = namespace["kernelized_" + func.__name__]
def __call__(self, *args, **kwargs):
return self.kernel(*args, **kwargs)
decompile, parse_to_ast și recompile sunt doar niște învelișuri subțiri în jurul funcțiilor standard ale bibliotecii (inspect, compile).
def decompile(self, func):
return inspect.getsource(func.__code__)
def parse_to_ast(self, source: str):
return compile(source, filename="<generated>", mode="exec", flags=ast.PyCF_ONLY_AST, dont_inherit=True)
def recompile(self, tree):
return compile(tree, filename="<generated>", mode="exec", dont_inherit=True)
Transformatorul este o subclasă a lui ast.NodeTransformer care traversează AST-ul și îl transformă într-un generator pentru model. Acesta este un model de design foarte generic care poate fi folosit pentru a transforma orice AST în orice alt AST.
class Transformer(ast.NodeTransformer):
"""
This subclass traverses the AST of the user-written, decorated,
model specification and transforms it into a generator for the
model. Subclassing in this way is the idiomatic way to transform
an AST.
Specifically:
1. rewrite all `if` statements into `with cc._if()` blocks
2. rename the function to `kernelized_` + original function name
3. Remove the @kernel decorator to prevent from recusion
"""
def visit_If(self, node):
self.generic_visit(node)
modified_node = ast.With(
items=[
ast.withitem(
context_expr=ast.Call(
func=ast.Attribute(
value=ast.Name(id="cc", ctx=ast.Load()),
attr="_if",
ctx=ast.Load(),
),
args=[node.test],
keywords=[],
),
optional_vars=None,
)
],
body=node.body,
)
ast.copy_location(modified_node, node)
ast.fix_missing_locations(modified_node)
return modified_node
def visit_FunctionDef(self, node):
modified_node = node
# Rename the function to `kernelized_` + original function name
modified_node.name = "kernelized_" + node.name
# Remove the @kernel decorator to prevent from recusion
modified_node.decorator_list = []
# Copy the source location of the original node
ast.copy_location(modified_node, node)
ast.fix_missing_locations(modified_node)
# Do not forget to visit the children of the node
self.generic_visit(node)
return modified_node
Introducerea conceptului de decorator „kernel” pentru a transforma o instrucțiune Python în QCS ISA poate fi confuză. Dar dacă cineva uită să aplice decoratorul? Codul kernel nu ar funcționa din cauza evaluării efectuate de interpretorul Python, ceea ce ar putea fi foarte confuz pentru dezvoltator.
Există însă multe soluții la această problemă. S-ar putea folosi un instrument de analiză statică, sau linter, pentru a verifica dacă funcția este decorată cu decoratorul kernel. Mersul în această direcție poate fi destul de puternic, mai ales atunci când se începe utilizarea LLM-urilor pentru a automatiza rescrierea codului.
(sursa imaginii: Încorporarea modificărilor de cod în funcție de context)
În această scurtă prezentare, am arătat că este posibil să rescrii AST-ul unei funcții Python într-un mod foarte simplu. Folosind această metodă, devine mult mai ușor să scrii nuclee cuantice care pot fi executate pe stiva de control cuantic, fără a fi nevoie să folosești sintaxa barbară with _xxx.
Desigur, ne putem întreba care este rostul manipulării AST în Python, din moment ce industria se îndreaptă rapid către o stivă unificată bazată pe LLVM cu dialecte MLIR pentru calculul cuantic. Acest lucru este într-adevăr adevărat, iar în nota următoare, voi arăta cum se utilizează MLIR pentru a obține același rezultat.
Va trebui să arunc o privire și la compilatorul NAC3 de la M-Labs. Comparativ cu CudaQ, NAC3 folosește o implementare bazată pe Rust. Chiar și codul Python este convertit în AST folosind un parser Rust. Cu toate acestea, generarea de cod este delegată către LLVM, cel puțin pentru soft-core-ul lor RISC-V.
Între timp, acest memo a fost o modalitate distractivă de a învăța despre AST-ul Python și poate fi util pentru îmbunătățirea compilatoarelor de circuite cuantice bazate pe Python existente care nu utilizează încă LLVM!