diff --git a/docs/source/_images/trasformations/qlasskit_transformations.odg b/docs/source/_images/trasformations/qlasskit_transformations.odg
index 63386e5e..c190a791 100644
Binary files a/docs/source/_images/trasformations/qlasskit_transformations.odg and b/docs/source/_images/trasformations/qlasskit_transformations.odg differ
diff --git a/docs/source/_images/trasformations/qlasskit_transformations.png b/docs/source/_images/trasformations/qlasskit_transformations.png
index ebf860ac..cd18085e 100644
Binary files a/docs/source/_images/trasformations/qlasskit_transformations.png and b/docs/source/_images/trasformations/qlasskit_transformations.png differ
diff --git a/docs/source/_images/trasformations/qlasskit_transformations.svg b/docs/source/_images/trasformations/qlasskit_transformations.svg
index a60ce4f8..f47788f0 100644
--- a/docs/source/_images/trasformations/qlasskit_transformations.svg
+++ b/docs/source/_images/trasformations/qlasskit_transformations.svg
@@ -1,258 +1,864 @@
-
-
-
diff --git a/docs/source/how_it_works.ipynb b/docs/source/how_it_works.ipynb
index 42ecb130..8192df20 100644
--- a/docs/source/how_it_works.ipynb
+++ b/docs/source/how_it_works.ipynb
@@ -8,9 +8,8 @@
"\n",
"To convert Python code into a quantum circuit, qlasskit implements a series of transformations:\n",
"\n",
- "1. It begins with the Python *AST* (Abstract Syntax Tree), converting it into a more streamlined form using the `ast2ast` module. \n",
- "2. Next, the streamlined AST is translated into *boolean expressions* as an intermediate step by the `ast2logic` module. \n",
- "During this phase, boolean expressions are refined and optimized in preparation for the final transformation.\n",
+ "1. It begins with the Python *AST* (Abstract Syntax Tree), converting it into *boolean expressions* as an intermediate step by the `ast2logic` module.\n",
+ "2. Boolean expressions are refined and optimized in preparation for the final transformation.\n",
"3. Finally, the `compiler` module takes these optimized boolean expressions and compiles them into a \n",
"*quantum circuit*.\n",
"\n",
diff --git a/pyproject.toml b/pyproject.toml
index 0f378ee7..adda2268 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -67,7 +67,6 @@ include = [
"qlasskit.boolopt",
"qlasskit.types",
"qlasskit.ast2logic",
- "qlasskit.ast2ast",
"qlasskit.qcircuit",
"qlasskit.compiler",
"qlasskit.algorithms",
diff --git a/qlasskit/__init__.py b/qlasskit/__init__.py
index 81e0c566..9514fba4 100644
--- a/qlasskit/__init__.py
+++ b/qlasskit/__init__.py
@@ -19,8 +19,7 @@
from .qcircuit import QCircuit, SupportedFrameworks, SupportedFramework # noqa: F401
from .qlassfun import QlassF, qlassf, qlassfa # noqa: F401
-from .ast2ast import ast2ast # noqa: F401
-from .ast2logic import exceptions # noqa: F401
+from .ast2logic import exceptions, preprocess_ast # noqa: F401
from .types import ( # noqa: F401, F403
const_to_qtype,
interpret_as_qtype,
diff --git a/qlasskit/ast2ast/__init__.py b/qlasskit/ast2ast/__init__.py
deleted file mode 100644
index 1504b3c5..00000000
--- a/qlasskit/ast2ast/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-from .ast2ast import ast2ast # noqa: F401
-from .astrewriter import ASTRewriter # noqa: F401
-from .constantfolder import ConstantFolder # noqa: F401
diff --git a/qlasskit/ast2ast/ast2ast.py b/qlasskit/ast2ast/ast2ast.py
deleted file mode 100644
index a91a9df2..00000000
--- a/qlasskit/ast2ast/ast2ast.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-import ast
-
-from .astrewriter import ASTRewriter
-from .constantfolder import ConstantFolder
-from .replacemultitargetassign import ReplaceMultiTargetAssign
-from .replacetypeann import ReplaceTypeAnn
-
-
-def ast2ast(a_tree: ast.AST) -> ast.AST:
- # print(ast.dump(a_tree))
-
- # Fold constants
- a_tree = ConstantFolder().visit(a_tree)
-
- # Replace Type Annotations
- a_tree = ReplaceTypeAnn().visit(a_tree)
-
- # Replace multi-target assign
- a_tree = ReplaceMultiTargetAssign().visit(a_tree)
-
- # Rewrite the ast
- a_tree = ASTRewriter().visit(a_tree)
-
- # Fold constants again
- a_tree = ConstantFolder().visit(a_tree)
-
- # print(ast.dump(a_tree))
- return a_tree
diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py
deleted file mode 100644
index ec6ace7a..00000000
--- a/qlasskit/ast2ast/astrewriter.py
+++ /dev/null
@@ -1,576 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-import ast
-import copy
-from dataclasses import dataclass
-from typing import Any, Optional, Union
-
-from ..ast2logic import flatten
-from .constantfolder import ConstantFolder
-from .env import Environment
-
-
-def iterator_accessor(
- n_name: str, i_name: str, i_max: int
-) -> Union[ast.Subscript, ast.IfExp]:
- """Given a List `n_name`, an index `i_name` returns L[0] if i == 0 else L[1] if i == 1 ..."""
- res: Union[ast.Subscript, ast.IfExp] = ast.Subscript(
- value=ast.Name(id=n_name, ctx=ast.Load()),
- slice=ast.Constant(value=0),
- ctx=ast.Load(),
- )
- for i in range(1, i_max + 1):
- cmp_i = ast.Compare(
- left=ast.Name(id=i_name, ctx=ast.Load()),
- ops=[ast.Eq()],
- comparators=[ast.Constant(value=i)],
- )
- res = ast.IfExp(
- test=cmp_i,
- body=ast.Subscript(
- value=ast.Name(id=n_name, ctx=ast.Load()),
- slice=ast.Constant(value=i),
- ctx=ast.Load(),
- ),
- orelse=res,
- )
-
- return res
-
-
-def iterator_accessor2(
- n_name: str,
- i_name: str,
- i_max: int,
- j_name: str,
- j_max: int,
-) -> Union[ast.Subscript, ast.IfExp]:
- """Given a List of List `n_name`, an index `i_name` and an optional index `j_name`,
- returns L[0][0] if i == 0 and j == 0 else L[0][1] if i == 0 and j == 1 ..."""
-
- def access_ij(i: int, j: int) -> ast.Subscript:
- "Return [i][j] subscript of name"
- return ast.Subscript(
- value=ast.Subscript(
- value=ast.Name(id=n_name, ctx=ast.Load()),
- slice=ast.Constant(value=i),
- ctx=ast.Load(),
- ),
- slice=ast.Constant(value=j),
- ctx=ast.Load(),
- )
-
- res: Union[ast.Subscript, ast.IfExp] = access_ij(0, 0)
-
- for i in range(i_max + 1):
- for j in range(j_max + 1):
- if i == 0 and j == 0:
- continue
-
- cmp_i = ast.Compare(
- left=ast.Name(id=i_name, ctx=ast.Load()),
- ops=[ast.Eq()],
- comparators=[ast.Constant(value=i)],
- )
-
- cmp_j = ast.Compare(
- left=ast.Name(id=j_name, ctx=ast.Load()),
- ops=[ast.Eq()],
- comparators=[ast.Constant(value=j)],
- )
-
- res = ast.IfExp(
- test=ast.BoolOp(
- op=ast.And(),
- values=[cmp_i, cmp_j],
- ),
- body=access_ij(i, j),
- orelse=res,
- )
-
- return res
-
-
-@dataclass
-class IsNamePresent(ast.NodeVisitor):
- """Check if a tree contains a specific name_id"""
-
- name_id: str
- present: bool = False
-
- def visit_Name(self, node: ast.Name):
- if node.id == self.name_id:
- self.present = True
-
-
-@dataclass
-class NameValReplacer(ast.NodeTransformer):
- """Replace all Name with name_id with the given val"""
-
- name_id: str
- val: Any
-
- def visit_Name(self, node: ast.Name):
- if node.id == self.name_id:
- return self.val
-
- return node
-
-
-class ASTRewriter(ast.NodeTransformer):
- """Rewrites the ast to a simplified version"""
-
- env: Environment
-
- def __init__(self, env: Optional[Environment] = None, ret=None):
- self.env = Environment() if env is None else env
- self.ret = None
- self._uniqd = 1
- self.__call_dispatch = {
- "range": self.__call_range,
- "len": self.__call_len,
- "sum": self.__call_sum,
- "ord": self.__call_ord,
- "chr": self.__call_chr,
- "any": self.__call_anyall,
- "all": self.__call_anyall,
- "min": self.__call_minmax,
- "max": self.__call_minmax,
- }
-
- @property
- def uniqd(self):
- """Return an unique identifier as str"""
- self._uniqd += 1
- return f"{self._uniqd:x}"
-
- def visit_Subscript(self, node: ast.Subscript): # noqa: C901
- # Replace L[a] with const a, to L[const]
- if isinstance(node.slice, ast.Name) and self.env.has_constant(node.slice.id):
- node.slice = self.env.get_constant(node.slice.id)
-
- # Handle inner access L[i]
- elif isinstance(node.value, ast.Name) and isinstance(node.slice, ast.Name):
- nname = node.value.id
- iname = node.slice.id
-
- # Infer i and j sizes from env['a']
- gtype = self.env.get_type(nname)
-
- if isinstance(gtype, ast.Tuple):
- max_i = len(gtype.elts) - 1
- else:
- outer_tuple = gtype.slice
- max_i = len(outer_tuple.elts) - 1
-
- # Create the IfExp structure
- return iterator_accessor(nname, iname, max_i)
-
- # Handle inner access L[i][j]
- elif (
- isinstance(node.value, ast.Subscript)
- and isinstance(node.value.value, ast.Name)
- and isinstance(node.value.slice, ast.Name)
- and isinstance(node.slice, ast.Name)
- ):
- nname = node.value.value.id
- iname = node.value.slice.id
- jname = node.slice.id
-
- # Infer i and j sizes from env['a']
- gtype = self.env.get_type(nname)
-
- if isinstance(gtype, ast.Tuple) and isinstance(gtype.elts[0], ast.Tuple):
- max_i = len(gtype.elts) - 1
- max_j = len(gtype.elts[0].elts) - 1
- else:
- outer_tuple = gtype.slice
- max_i = len(outer_tuple.elts) - 1
- inner_tuple = outer_tuple.elts
- max_j = len(inner_tuple) - 1
-
- # Create the IfExp structure
- return iterator_accessor2(nname, iname, max_i, jname, max_j)
-
- # Unroll L[a] with (L[0] if a == 0 else L[1] if a == 1 ...) when L is constant
- elif (
- isinstance(node.slice, ast.Name)
- and not self.env.has_constant(node.slice.id)
- ) or isinstance(node.slice, ast.Subscript):
- if isinstance(node.value, ast.Name):
- if node.value.id == "Tuple":
- return node
-
- tup = self.env.get_constant(node.value.id)
- else:
- tup = node.value
-
- if isinstance(tup, ast.Constant):
- tup = tup.value
-
- if not isinstance(tup, ast.Tuple):
- raise Exception(
- "Not a tuple in ast2ast visit subscript with not constant node.slice: "
- + ast.dump(tup)
- )
-
- elts = tup.elts
-
- ifex = elts[0]
-
- for i, x in enumerate(elts[1:]):
- ifex = ast.IfExp(
- test=ast.Compare(
- left=node.slice,
- ops=[ast.Eq()],
- comparators=[ast.Constant(value=i + 1)],
- ),
- body=x,
- orelse=ifex,
- )
- return ifex
-
- return node
-
- def visit_Name(self, node: ast.Name):
- # __ prefix is reserved for internal use
- if node.id.startswith("__"):
- raise Exception("invalid name starting with __")
-
- return node
-
- def visit_If(self, node: ast.If):
- """Replace if(c,t,e) with if(c,t1), if(c,t2), ..., if(!c, e1), ..."""
- body = flatten([self.visit(n) for n in node.body])
- orelse = flatten([self.visit(n) for n in node.orelse])
- test_name = "_iftarg" + self.uniqd
-
- if_l = [
- ast.Assign(
- targets=[ast.Name(id=test_name)],
- value=self.visit(node.test),
- )
- ]
-
- for b in body:
- if not isinstance(b, ast.Assign):
- raise Exception("if body only allows assigns: ", ast.dump(b))
-
- if len(b.targets) != 1 or not isinstance(b.targets[0], ast.Name):
- raise Exception("if targets only allow one Name target: ", ast.dump(b))
-
- target = b.targets[0].id
-
- if target.startswith("__") and target not in self.env:
- orelse_inner = ast.Name(id=target[2:])
- else:
- orelse_inner = ast.Name(id=target)
-
- if_l.append(
- ast.Assign(
- targets=b.targets,
- value=ast.IfExp(
- test=ast.Name(id=test_name), body=b.value, orelse=orelse_inner
- ),
- )
- )
-
- for b in orelse:
- if not isinstance(b, ast.Assign):
- raise Exception("if body only allows assigns: ", ast.dump(b))
-
- if len(b.targets) != 1 or not isinstance(b.targets[0], ast.Name):
- raise Exception("if targets only allow one Name target: ", ast.dump(b))
-
- target = b.targets[0].id
-
- if target.startswith("__") and target not in self.env:
- orelse_inner = ast.Name(id=target[2:])
- elif target[0 : len("_iftarg")] == "_iftarg":
- if_l.append(b)
- continue
- else:
- orelse_inner = ast.Name(id=target)
-
- if_l.append(
- ast.Assign(
- targets=b.targets,
- value=ast.IfExp(
- test=ast.Name(id=test_name), orelse=b.value, body=orelse_inner
- ),
- )
- )
-
- return if_l
-
- def visit_List(self, node: ast.List):
- """Converts List to Tuple"""
- return ast.Tuple(elts=[self.visit(el) for el in node.elts])
-
- def visit_AnnAssign(self, node: ast.AnnAssign):
- node.value = self.visit(node.value) if node.value else node.value
-
- if hasattr(node.target, "id"):
- self.env.set_type(node.target.id, node.annotation)
-
- return node
-
- def visit_FunctionDef(self, node):
- for x in node.args.args:
- self.env.set_type(x.arg, x.annotation)
-
- self.ret = node.returns
-
- return super().generic_visit(node)
-
- def visit_Assign(self, node: ast.Assign):
- if not hasattr(node.targets[0], "id"):
- raise ValueError(node.targets[0], "has no id")
-
- target = node.targets[0].id
- was_known = target in self.env
-
- if isinstance(node.value, ast.Constant):
- self.env.set_constant(target, node.value)
- elif isinstance(node.value, ast.Name) and node.value.id in self.env:
- self.env.copy_type(node.value.id, target)
- elif isinstance(node.value, ast.Tuple) or isinstance(node.value, ast.List):
- self.env.set_constant(target, self.visit(node.value))
- else:
- self.env.set_type(target, "Unknown") # type: ignore
-
- # If value is not self referencing, we can skip this (ie: a = b + 1)
- ip = IsNamePresent(target)
- ip.visit(node.value)
-
- # Reassigning an already present variable (use a temp variable)
- if ip.present and was_known and not isinstance(node.value, ast.Constant):
- new_targ = ast.Name(id=f"__{target}", ctx=ast.Load())
-
- return [
- ast.Assign(
- targets=[new_targ],
- value=self.visit(node.value),
- ),
- ast.Assign(
- targets=node.targets,
- value=new_targ,
- ),
- ]
-
- node.value = self.visit(node.value)
- return node
-
- def visit_AugAssign(self, node: ast.AugAssign):
- """Translate AugAssign to Assign + BinOp (+=, -=, etc)"""
- # Reassigning an already present variable (use a temp variable)
- # if node.target.id in self.env:
- if not hasattr(node.target, "id"):
- raise ValueError(node.target, "has no id")
-
- new_targ = ast.Name(id=f"__{node.target.id}", ctx=ast.Load())
-
- return [
- ast.Assign(
- targets=[new_targ],
- value=self.visit(
- ast.BinOp(left=node.target, op=node.op, right=node.value)
- ),
- ),
- ast.Assign(
- targets=[node.target],
- value=new_targ,
- ),
- ]
-
- def __unroll_arg(
- self, arg: Union[ast.Constant, ast.Subscript, ast.Tuple, ast.Name]
- ):
- """Transform a node to a list (when is a Tuple or a subscribable type)"""
- if isinstance(arg, ast.Tuple):
- # If it's a tuple, return elts
- return arg.elts
- elif isinstance(arg, ast.Constant) and isinstance(arg.value, ast.Tuple):
- return arg.value.elts
- elif isinstance(arg, ast.Subscript) and isinstance(arg.value, ast.Name):
- _sval = self.env.get_type(arg.value.id)
- if (
- isinstance(_sval, ast.Subscript)
- and isinstance(_sval.slice, ast.Tuple)
- and isinstance(arg.slice, ast.Constant)
- ):
- return [
- ast.Subscript(
- value=ast.Subscript(
- value=ast.Name(id=arg.value.id, ctx=ast.Load()),
- slice=ast.Constant(value=arg.slice.value, kind=None),
- ),
- slice=ast.Constant(value=i, kind=None),
- )
- for i in range(len(_sval.slice.elts))
- ]
- elif isinstance(arg, ast.Name):
- # If it's a name, is in env and is a Tuple, return elements
- if (
- self.env.has_type(arg.id)
- and isinstance(self.env.get_type(arg.id), ast.Subscript)
- and self.env.get_type(arg.id).value.id == "Tuple"
- ):
- _sval = self.env.get_type(arg.id).slice
-
- return [
- ast.Subscript(
- value=ast.Name(id=arg.id, ctx=ast.Load()),
- slice=ast.Constant(value=i, kind=None),
- )
- for i in range(len(_sval.elts))
- ]
- # If it's a tuple constant, return elements
- elif self.env.has_constant(arg.id) and isinstance(
- self.env.get_type(arg.id), ast.Tuple
- ):
- return self.env.get_constant(arg.id).elts
- return [arg]
-
- def visit_For(self, node: ast.For):
- """Unroll for loops to single iterations"""
- if not hasattr(node.target, "id"):
- raise ValueError(node.target, "has no id")
-
- iter = self.__unroll_arg(self.visit(node.iter))
- rolls = []
- iter = flatten(iter)
-
- for i in iter:
- if isinstance(i, ast.Constant) or isinstance(i, ast.Subscript):
- _val = i
- else:
- _val = ast.Constant(value=i)
- self.env.set_constant(node.target.id, _val)
-
- tar_assign = self.visit(ast.Assign(targets=[node.target], value=_val))
- rolls.extend(flatten([tar_assign]))
-
- new_body = [
- NameValReplacer(node.target.id, _val).visit(copy.deepcopy(b))
- for b in node.body
- ]
- rolls.extend(flatten([self.visit(copy.deepcopy(b)) for b in new_body]))
-
- return rolls
-
- def __call_range(self, node):
- node = ConstantFolder().visit(node)
-
- if not all([isinstance(a, ast.Constant) for a in node.args]):
- raise Exception("Range call on not constant arguments is not handled")
-
- args = [a.value for a in node.args]
- it = list(range(*args))
- return it
-
- def __call_len(self, node):
- if len(node.args) != 1:
- raise Exception("Len only receives one argument")
-
- args = self.__unroll_arg(node.args[0])
- return ast.Constant(value=len(args))
-
- def __call_minmax(self, node):
- if len(node.args) == 1:
- args = self.__unroll_arg(node.args[0])
- else:
- args = node.args
-
- op = ast.Gt() if node.func.id == "max" else ast.LtE()
-
- def iterif(arg_l):
- if len(arg_l) == 1:
- return arg_l[0]
- else:
- comps = [
- ast.Compare(left=arg_l[0], ops=[op], comparators=[l_it])
- for l_it in arg_l[1:]
- ]
- comp = ast.BoolOp(op=ast.And(), values=comps) # type: ignore
- return ast.IfExp(test=comp, body=arg_l[0], orelse=iterif(arg_l[1:]))
-
- return iterif(args)
-
- def __call_sum(self, node):
- if len(node.args) != 1:
- raise Exception(f"sum() takes at most 1 argument ({len(node.args)} given)")
-
- args = self.__unroll_arg(node.args[0])
-
- def iterif(arg_l):
- if len(arg_l) == 1:
- return arg_l[0]
- else:
- return ast.BinOp(left=arg_l[0], op=ast.Add(), right=iterif(arg_l[1:]))
-
- return iterif(args)
-
- def __call_anyall(self, node):
- if len(node.args) != 1:
- raise Exception(f"any() takes exactly 1 argument ({len(node.args)} given)")
-
- args = self.__unroll_arg(node.args[0])
- op = ast.Or() if node.func.id == "any" else ast.And()
- return ast.BoolOp(op=op, values=args)
-
- def __call_chr(self, node):
- if len(node.args) != 1:
- raise Exception(f"chr() takes exactly 1 argument ({len(node.args)} given)")
- return node.args[0]
-
- def __call_ord(self, node):
- if len(node.args) != 1:
- raise Exception(f"ord() takes exactly 1 argument ({len(node.args)} given)")
- return node.args[0]
-
- def visit_Call(self, node: ast.Call):
- node.args = [self.visit(ar) for ar in node.args]
- if not hasattr(node.func, "id"):
- return node
-
- if node.func.id == "print":
- return None
-
- handler = self.__call_dispatch.get(node.func.id)
- return handler(node) if handler is not None else node
-
- def visit_BinOp(self, node: ast.BinOp):
- # Rewrite the ** operator to be a series of multiplications
- if isinstance(node.op, ast.Pow):
- if (
- isinstance(node.right, ast.Constant)
- and isinstance(node.right.value, int)
- and node.right.value > 0
- ):
- result = node.left
- for _ in range(node.right.value - 1):
- result = ast.BinOp(left=result, op=ast.Mult(), right=node.left)
- return result
-
- elif (
- isinstance(node.right, ast.Constant)
- and isinstance(node.right.value, int)
- and node.right.value == 0
- ):
- return ast.Constant(value=1)
-
- return super().generic_visit(node)
diff --git a/qlasskit/ast2ast/env.py b/qlasskit/ast2ast/env.py
deleted file mode 100644
index d2e63e36..00000000
--- a/qlasskit/ast2ast/env.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-import ast
-from typing import Any, Dict, Union
-
-
-class Environment:
- types: Dict[str, ast.AST]
- constants: Dict[str, Any]
-
- def __init__(self):
- self.types = {}
- self.constants = {}
-
- def set_type(self, name: str, type_annotation: ast.AST) -> None:
- self.types[name] = type_annotation
-
- def set_constant(self, name: str, value: Union[ast.Constant, Any]) -> None:
- if isinstance(value, ast.Constant):
- return self.set_constant(name, value.value)
-
- self.constants[name] = value
-
- if name not in self.types:
- self.types[name] = value
-
- def copy_type(self, origin: str, dest: str) -> None:
- self.types[dest] = self.types[origin]
-
- def get_type(self, name: str): # -> Optional[ast.AST]:
- return self.types.get(name)
-
- def get_constant(self, name: str): # -> Optional[Any]:
- return self.constants.get(name)
-
- def has_type(self, name: str) -> bool:
- return name in self.types
-
- def has_constant(self, name: str) -> bool:
- return name in self.constants
-
- def remove(self, name: str) -> None:
- self.types.pop(name, None)
- self.constants.pop(name, None)
-
- def __contains__(self, name: str):
- return name in self.types or name in self.constants
-
- def __getitem__(self, name: str) -> Union[Any, ast.AST]:
- if name in self.constants:
- return self.constants[name]
- return self.types.get(name)
-
- def __setitem__(self, name: str, value: Union[ast.Constant, Any, ast.AST]) -> None:
- if isinstance(value, ast.AST):
- self.set_type(name, value)
- else:
- self.set_constant(name, value)
diff --git a/qlasskit/ast2ast/replacemultitargetassign.py b/qlasskit/ast2ast/replacemultitargetassign.py
deleted file mode 100644
index 7faa6b76..00000000
--- a/qlasskit/ast2ast/replacemultitargetassign.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-import ast
-
-
-class ReplaceMultiTargetAssign(ast.NodeTransformer):
- def visit_Assign(self, node: ast.Assign):
- if len(node.targets) != 1 or not hasattr(node.targets[0], "elts"):
- return node
-
- # Transform multi-target assign to single target assigns
- if isinstance(node.value, ast.Name):
- return [
- self.visit(
- ast.Assign(
- targets=[ast.Name(id=node.targets[0].elts[i].id)],
- value=ast.Subscript(
- value=node.value, slice=ast.Constant(value=i)
- ),
- )
- )
- for i in range(len(node.targets[0].elts))
- ]
-
- _temptup = self.visit(
- ast.Assign(targets=[ast.Name(id="_temptup")], value=node.value)
- )
-
- single_assigns = [
- self.visit(
- ast.Assign(
- targets=[ast.Name(id=node.targets[0].elts[i].id)],
- value=ast.Subscript(
- value=ast.Name(id="_temptup"), slice=ast.Constant(value=i)
- ),
- )
- )
- for i in range(len(node.targets[0].elts))
- ]
-
- return [_temptup] + single_assigns
diff --git a/qlasskit/ast2ast/replacetypeann.py b/qlasskit/ast2ast/replacetypeann.py
deleted file mode 100644
index 065c962a..00000000
--- a/qlasskit/ast2ast/replacetypeann.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-import ast
-import copy
-from typing import Union
-
-
-class TypeAnnotationsReplacer(ast.NodeTransformer):
- def visit_Subscript(self, ann: ast.Subscript):
- if not isinstance(ann.value, ast.Name) or not hasattr(ann.slice, "elts"):
- return ann
-
- if ann.value.id == "Tuple":
- _elts = ann.slice.elts
- _ituple = ast.Tuple(elts=[self.visit(el) for el in _elts])
-
- return ast.Subscript(
- value=ast.Name(id="Tuple", ctx=ast.Load()),
- slice=_ituple,
- )
-
- # Replace Qlist[T,n] with Tuple[(T,)*n]
- elif ann.value.id == "Qlist":
- _elts = ann.slice.elts
- _ituple = ast.Tuple(elts=[copy.deepcopy(_elts[0])] * _elts[1].value)
-
- return ast.Subscript(
- value=ast.Name(id="Tuple", ctx=ast.Load()),
- slice=_ituple,
- )
-
- # Replace Qmatrix[T,n,m] with Tuple[(Tuple[(T,)*m],)*n]
- elif ann.value.id == "Qmatrix":
- _elts = ann.slice.elts
- _ituple_row = ast.Tuple(elts=[copy.deepcopy(_elts[0])] * _elts[2].value)
- _ituple = ast.Tuple(elts=[copy.deepcopy(_ituple_row)] * _elts[1].value)
-
- return ast.Subscript(
- value=ast.Name(id="Tuple", ctx=ast.Load()),
- slice=_ituple,
- )
-
- return ann
-
- def visit_Name(self, ann: ast.Name):
- res_ann: Union[ast.Name, ast.Subscript]
-
- # Replace QintX with Qint[X]
- if ann.id[:4] == "Qint":
- res_ann = ast.Subscript(
- value=ast.Name(id="Qint", ctx=ast.Load()),
- slice=ast.Constant(value=int(ann.id[4:])),
- )
-
- # Replace QfixedX with Qfixed[X]
- elif ann.id[:6] == "Qfixed":
- res_ann = ast.Subscript(
- value=ast.Name(id="Qfixed", ctx=ast.Load()),
- slice=ast.Constant(value=int(ann.id[6:])),
- )
-
- else:
- res_ann = ann
-
- return res_ann
-
-
-def _replace_types_annotations(ann, arg=None):
- """Replaces type annotations, translating high level types"""
- if ann is not None:
- ann = TypeAnnotationsReplacer().visit(ann)
-
- if arg is not None:
- arg.annotation = ann
- return arg
- else:
- return ann
-
-
-class ReplaceTypeAnn(ast.NodeTransformer):
- def visit_AnnAssign(self, node: ast.AnnAssign):
- node.annotation = _replace_types_annotations(node.annotation)
- return node
-
- def visit_FunctionDef(self, node: ast.FunctionDef):
- node.args.args = [
- _replace_types_annotations(x.annotation, arg=x) for x in node.args.args
- ]
-
- node.returns = _replace_types_annotations(node.returns)
-
- return node
diff --git a/qlasskit/ast2logic/__init__.py b/qlasskit/ast2logic/__init__.py
index 39cb07b9..fc40e70c 100644
--- a/qlasskit/ast2logic/__init__.py
+++ b/qlasskit/ast2logic/__init__.py
@@ -22,4 +22,5 @@
from .t_expression import translate_expression, decompose_to_symbols # noqa: F401, E402
from .t_statement import translate_statement # noqa: F401, E402
from .t_ast import translate_ast # noqa: F401, E402
+from .preprocess import preprocess_ast # noqa: F401, E402
from . import exceptions # noqa: F401, E402
diff --git a/qlasskit/ast2logic/env.py b/qlasskit/ast2logic/env.py
index c0be5e3e..29de6b3f 100644
--- a/qlasskit/ast2logic/env.py
+++ b/qlasskit/ast2logic/env.py
@@ -15,7 +15,7 @@
# along with this program. If not, see .
import sys
-from typing import Dict, List, Tuple
+from typing import Any, Dict, List, Tuple
from sympy import Symbol
from sympy.logic.boolalg import Boolean
@@ -37,11 +37,13 @@ class Env:
bindings: List[Binding]
types: List[TypeBinding]
defs: List[LogicFun]
+ consts: Dict[str, Any]
def __init__(self) -> None:
self.bindings = []
self.types = []
self.defs = []
+ self.consts = {}
for t in BUILTIN_TYPES:
self.bind_type((t.__name__, t)) # type: ignore
@@ -107,6 +109,18 @@ def getdef(self, fun_name: str) -> LogicFun:
except:
raise exceptions.UnboundException(fun_name, self)
+ def set_constant(self, name: str, value: Any) -> None:
+ self.consts[name] = value
+
+ def get_constant(self, name: str) -> Any:
+ return self.consts[name]
+
+ def has_constant(self, name: str) -> bool:
+ return name in self.consts
+
+ def remove_constant(self, name: str) -> None:
+ self.consts.pop(name, None)
+
def bind(self, bb: Binding, rebind=False) -> None:
if not rebind and bb.name in self:
raise Exception("duplicate bind")
diff --git a/qlasskit/ast2ast/constantfolder.py b/qlasskit/ast2logic/preprocess.py
similarity index 52%
rename from qlasskit/ast2ast/constantfolder.py
rename to qlasskit/ast2logic/preprocess.py
index 196f7912..b42c78c7 100644
--- a/qlasskit/ast2ast/constantfolder.py
+++ b/qlasskit/ast2logic/preprocess.py
@@ -13,12 +13,18 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+"""AST preprocessing passes, applied before translate_ast: constant folding,
+high-level type annotation replacement and multi-target assign expansion."""
import ast
+import copy
import operator
+from typing import Union
class ConstantFolder(ast.NodeTransformer):
+ """Folds expression with constants to the result"""
+
def __init__(self):
self.builtin_funcs = {
"abs": abs,
@@ -134,3 +140,145 @@ def visit_IfExp(self, node: ast.IfExp):
if isinstance(node.test, ast.Constant):
return node.body if node.test.value else node.orelse
return node
+
+
+class TypeAnnotationsReplacer(ast.NodeTransformer):
+ def visit_Subscript(self, ann: ast.Subscript):
+ if not isinstance(ann.value, ast.Name) or not hasattr(ann.slice, "elts"):
+ return ann
+
+ if ann.value.id == "Tuple":
+ _elts = ann.slice.elts
+ _ituple = ast.Tuple(elts=[self.visit(el) for el in _elts])
+
+ return ast.Subscript(
+ value=ast.Name(id="Tuple", ctx=ast.Load()),
+ slice=_ituple,
+ )
+
+ # Replace Qlist[T,n] with Tuple[(T,)*n]
+ elif ann.value.id == "Qlist":
+ _elts = ann.slice.elts
+ _ituple = ast.Tuple(elts=[copy.deepcopy(_elts[0])] * _elts[1].value)
+
+ return ast.Subscript(
+ value=ast.Name(id="Tuple", ctx=ast.Load()),
+ slice=_ituple,
+ )
+
+ # Replace Qmatrix[T,n,m] with Tuple[(Tuple[(T,)*m],)*n]
+ elif ann.value.id == "Qmatrix":
+ _elts = ann.slice.elts
+ _ituple_row = ast.Tuple(elts=[copy.deepcopy(_elts[0])] * _elts[2].value)
+ _ituple = ast.Tuple(elts=[copy.deepcopy(_ituple_row)] * _elts[1].value)
+
+ return ast.Subscript(
+ value=ast.Name(id="Tuple", ctx=ast.Load()),
+ slice=_ituple,
+ )
+
+ return ann
+
+ def visit_Name(self, ann: ast.Name):
+ res_ann: Union[ast.Name, ast.Subscript]
+
+ # Replace QintX with Qint[X]
+ if ann.id[:4] == "Qint":
+ res_ann = ast.Subscript(
+ value=ast.Name(id="Qint", ctx=ast.Load()),
+ slice=ast.Constant(value=int(ann.id[4:])),
+ )
+
+ # Replace QfixedX with Qfixed[X]
+ elif ann.id[:6] == "Qfixed":
+ res_ann = ast.Subscript(
+ value=ast.Name(id="Qfixed", ctx=ast.Load()),
+ slice=ast.Constant(value=int(ann.id[6:])),
+ )
+
+ else:
+ res_ann = ann
+
+ return res_ann
+
+
+def _replace_types_annotations(ann, arg=None):
+ """Replaces type annotations, translating high level types"""
+ if ann is not None:
+ ann = TypeAnnotationsReplacer().visit(ann)
+
+ if arg is not None:
+ arg.annotation = ann
+ return arg
+ else:
+ return ann
+
+
+class ReplaceTypeAnn(ast.NodeTransformer):
+ def visit_AnnAssign(self, node: ast.AnnAssign):
+ node.annotation = _replace_types_annotations(node.annotation)
+ return node
+
+ def visit_FunctionDef(self, node: ast.FunctionDef):
+ node.args.args = [
+ _replace_types_annotations(x.annotation, arg=x) for x in node.args.args
+ ]
+
+ node.returns = _replace_types_annotations(node.returns)
+
+ return node
+
+
+class ReplaceMultiTargetAssign(ast.NodeTransformer):
+ def visit_Assign(self, node: ast.Assign):
+ if len(node.targets) != 1 or not hasattr(node.targets[0], "elts"):
+ return node
+
+ # Transform multi-target assign to single target assigns
+ if isinstance(node.value, ast.Name):
+ return [
+ self.visit(
+ ast.Assign(
+ targets=[ast.Name(id=node.targets[0].elts[i].id)],
+ value=ast.Subscript(
+ value=node.value, slice=ast.Constant(value=i)
+ ),
+ )
+ )
+ for i in range(len(node.targets[0].elts))
+ ]
+
+ _temptup = self.visit(
+ ast.Assign(targets=[ast.Name(id="_temptup")], value=node.value)
+ )
+
+ single_assigns = [
+ self.visit(
+ ast.Assign(
+ targets=[ast.Name(id=node.targets[0].elts[i].id)],
+ value=ast.Subscript(
+ value=ast.Name(id="_temptup"), slice=ast.Constant(value=i)
+ ),
+ )
+ )
+ for i in range(len(node.targets[0].elts))
+ ]
+
+ return [_temptup] + single_assigns
+
+
+def preprocess_ast(a_tree: ast.AST) -> ast.AST:
+ """Apply the preprocessing passes to the ast before translate_ast"""
+ # Fold constants
+ a_tree = ConstantFolder().visit(a_tree)
+
+ # Replace Type Annotations
+ a_tree = ReplaceTypeAnn().visit(a_tree)
+
+ # Replace multi-target assign
+ a_tree = ReplaceMultiTargetAssign().visit(a_tree)
+
+ # Fold constants again
+ a_tree = ConstantFolder().visit(a_tree)
+
+ return a_tree
diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py
index 18a9f330..d9c15cc7 100644
--- a/qlasskit/ast2logic/t_expression.py
+++ b/qlasskit/ast2logic/t_expression.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import ast
+import copy
from typing import List, Tuple, cast, get_args
from sympy import Symbol
@@ -46,12 +47,102 @@ def decompose_to_symbols(vlist, base, res: List = []) -> List[Symbol]:
class ExpressionTranslator(ast.NodeTransformer):
def __init__(self, env: Env):
self.env = env
+ self.__call_dispatch = {
+ "range": self.__call_range,
+ "len": self.__call_len,
+ "sum": self.__call_sum,
+ "ord": self.__call_ord,
+ "chr": self.__call_chr,
+ "any": self.__call_anyall,
+ "all": self.__call_anyall,
+ "min": self.__call_minmax,
+ "max": self.__call_minmax,
+ }
+
+ self.__binop_dispatch = {
+ ast.Add: "add",
+ ast.Sub: "sub",
+ ast.Mult: "mul",
+ ast.Mod: "mod",
+ ast.FloorDiv: "floor_div",
+ ast.BitXor: "bitwise_xor",
+ ast.BitAnd: "bitwise_and",
+ ast.BitOr: "bitwise_or",
+ }
# Name reference
def visit_Name(self, expr: ast.Name):
+ # __ prefix is reserved for internal use
+ if expr.id.startswith("__") and expr.id not in self.env:
+ raise Exception("invalid name starting with __")
+
binding = self.env[expr.id]
return (binding.ttype, binding.to_exp())
+ @staticmethod
+ def __py_to_ast(v) -> ast.expr:
+ if isinstance(v, (list, tuple)):
+ return ast.List(elts=[ExpressionTranslator.__py_to_ast(x) for x in v])
+ return ast.Constant(value=v)
+
+ def __const_chain(self, node, slices) -> ast.expr:
+ """Build an IfExp chain over a constant: for slices (innermost
+ first), constant slices navigate the value, the first dynamic slice
+ becomes an IfExp(i == k, elem_k, ...) chain"""
+ if not slices:
+ return self.__py_to_ast(node)
+ s, rest = slices[0], slices[1:]
+ if isinstance(s, ast.Constant):
+ return self.__const_chain(node[s.value], rest)
+ n = len(node)
+ chain = self.__const_chain(node[n - 1], rest)
+ for k in reversed(range(n - 1)):
+ chain = ast.IfExp(
+ test=ast.Compare(
+ left=copy.deepcopy(s),
+ ops=[ast.Eq()],
+ comparators=[ast.Constant(value=k)],
+ ),
+ body=self.__const_chain(node[k], rest),
+ orelse=chain,
+ )
+ return chain
+
+ @staticmethod
+ def __first_dim(ttype) -> int:
+ """First dimension of a type: BIT_SIZE for Qint-like, len(args) for Tuple"""
+ if hasattr(ttype, "BIT_SIZE"):
+ return ttype.BIT_SIZE # type: ignore
+ args = get_args(ttype)
+ return len(args) if args else 1
+
+ def __dyn_subscript_chain(self, expr: ast.Subscript) -> ast.IfExp:
+ """L[0] if i==0 else L[1] if i==1 ... else L[max] for a var idx (L[max] fallthrough)"""
+ if isinstance(expr.value, ast.Name):
+ vt = self.env[expr.value.id].ttype
+ else:
+ # nested subscript (m[i][j]): type of the subscripted value (the row)
+ vt = self.visit(expr.value)[0]
+ n = self.__first_dim(vt)
+
+ def access(k) -> ast.Subscript:
+ return ast.Subscript(
+ value=expr.value, slice=ast.Constant(value=k), ctx=ast.Load()
+ )
+
+ chain: ast.IfExp | ast.Subscript = access(n - 1)
+ for k in reversed(range(n - 1)):
+ chain = ast.IfExp(
+ test=ast.Compare(
+ left=copy.deepcopy(expr.slice),
+ ops=[ast.Eq()],
+ comparators=[ast.Constant(value=k)],
+ ),
+ body=access(k),
+ orelse=chain,
+ )
+ return chain # type: ignore
+
# Subscript: a[0][1]
def visit_Subscript(self, expr: ast.Subscript): # noqa: C901
def unroll_subscripts(sub: ast.Subscript, st):
@@ -73,9 +164,45 @@ def unroll_subscripts(sub: ast.Subscript, st):
_sval = expr.slice # type: ignore
+ # if the base of the subscript chain is a constant, unroll
+ base = expr.value
+ slices = []
+ while isinstance(base, ast.Subscript):
+ slices.append(base.slice)
+ base = base.value
+ slices.append(expr.slice)
+ slices.reverse() # innermost first
+ if isinstance(base, ast.Name) and self.env.has_constant(base.id):
+ return self.visit(
+ self.__const_chain(self.env.get_constant(base.id), slices)
+ )
+
if not isinstance(_sval, ast.Constant):
+ # variable index: build L[0] if i==0 else L[1] if ... else L[max] as an IfExp
+ # chain, sized from the base type's first dimension, then translate it
+ return self.visit(self.__dyn_subscript_chain(expr))
+
+ if isinstance(expr.value, ast.Subscript) and not isinstance(
+ expr.value.slice, ast.Constant
+ ):
+ # constant outer slice over a dynamic inner subscript (t[i][0]):
+ # translate the inner subscript first, then index into the result
+ vt, vexp = self.visit(expr.value)
+ if isinstance(vexp, list):
+ args = get_args(vt)
+ if args and _sval.value < len(args): # type: ignore
+ etype = args[_sval.value] # type: ignore
+ else:
+ etype = bool
+ esize = etype.BIT_SIZE if hasattr(etype, "BIT_SIZE") else 1
+ elem = vexp[_sval.value * esize : (_sval.value + 1) * esize] # type: ignore
+ return (etype, elem if esize > 1 else elem[0])
raise exceptions.ExpressionNotHandledException(expr)
+ if isinstance(expr.value, (ast.Tuple, ast.List)):
+ # constant tuple/list literal subscript: pick the element directly
+ return self.visit(expr.value.elts[_sval.value]) # type: ignore
+
if isinstance(expr.value, ast.Name):
sn = f"{expr.value.id}.{safe_str(_sval.value)}"
else:
@@ -106,8 +233,14 @@ def unroll_subscripts(sub: ast.Subscript, st):
inner_type,
[Symbol(f"{sn}.{i}") for i in range(inner_type.BIT_SIZE)],
)
+ elif inner_type == bool:
+ return (bool, Symbol(sn))
else:
- return (inner_type, Symbol(sn))
+ # tuple element: collect its bits from the base binding's bitvec
+ bits = [
+ b for b in self.env[sn.split(".")[0]].bitvec if b.startswith(sn + ".")
+ ]
+ return (inner_type, [Symbol(b) for b in bits])
# Boolop: and, or
def visit_BoolOp(self, expr: ast.BoolOp):
@@ -199,6 +332,11 @@ def visit_Tuple(self, expr: ast.Tuple):
return (Tuple[tuple(tlts)], elts) # type: ignore
+ # List
+ def visit_List(self, node: ast.List):
+ """Converts List to Tuple"""
+ return self.visit_Tuple(ast.Tuple(elts=node.elts, ctx=node.ctx))
+
# Compare operator
def visit_Compare(self, expr: ast.Compare): # noqa: C901
if len(expr.ops) != 1 or len(expr.comparators) != 1:
@@ -265,8 +403,7 @@ def visit_Compare(self, expr: ast.Compare): # noqa: C901
# Binop
def visit_BinOp(self, expr: ast.BinOp): # noqa: C901
- # Sub | Mult | MatMult | Div | Mod | Pow | FloorDiv
- # print(ast.dump(expr))
+ # Sub | Mult | MatMult | Div | Mod | Pow | FloorDiv | Xor | And | Or
tleft = self.visit(expr.left)
tright = self.visit(expr.right)
@@ -278,22 +415,23 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901
elif isinstance(expr.op, ast.BitOr):
return bool, Or(tleft[1], tright[1])
- if isinstance(expr.op, ast.Add) and hasattr(tleft[0], "add"):
- return tleft[0].add(tleft, tright)
- elif isinstance(expr.op, ast.Sub) and hasattr(tleft[0], "sub"):
- return tleft[0].sub(tleft, tright)
- elif isinstance(expr.op, ast.Mult) and hasattr(tleft[0], "mul"):
- return tleft[0].mul(tleft, tright)
- elif isinstance(expr.op, ast.Mod):
- return tleft[0].mod(tleft, tright) # type: ignore
- elif isinstance(expr.op, ast.FloorDiv) and hasattr(tleft[0], "floor_div"):
- return tleft[0].floor_div(tleft, tright)
- elif isinstance(expr.op, ast.BitXor) and hasattr(tleft[0], "bitwise_xor"):
- return tleft[0].bitwise_xor(tleft, tright)
- elif isinstance(expr.op, ast.BitAnd) and hasattr(tleft[0], "bitwise_and"):
- return tright[0].bitwise_and(tleft, tright) # type: ignore
- elif isinstance(expr.op, ast.BitOr) and hasattr(tleft[0], "bitwise_or"):
- return tleft[0].bitwise_or(tleft, tright)
+ if isinstance(expr.op, ast.Pow) and hasattr(tleft[0], "mul"):
+ # Transform pow to multiplications
+ if not isinstance(expr.right, ast.Constant) and not isinstance(
+ expr.right.value, int # type: ignore
+ ):
+ raise exceptions.ExpressionNotHandledException(expr.right)
+
+ if expr.right.value > 0: # type: ignore
+ result = tleft
+ for _ in range(expr.right.value - 1): # type: ignore
+ result = result[0].mul(result, tleft)
+
+ return result
+ elif expr.right.value == 0: # type: ignore
+ return const_to_qtype(1)
+ else:
+ raise exceptions.ExpressionNotHandledException(expr.right)
elif (
isinstance(expr.op, ast.LShift)
and hasattr(tleft[0], "shift_left")
@@ -307,7 +445,118 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901
):
return tleft[0].shift_right(tleft, expr.right.value) # type: ignore
else:
- raise exceptions.ExpressionNotHandledException(expr)
+ mt = self.__binop_dispatch.get(type(expr.op))
+
+ if mt is not None and hasattr(tleft[0], mt):
+ return getattr(tleft[0], mt)(tleft, tright)
+ else:
+ raise exceptions.ExpressionNotHandledException(expr)
+
+ def __unroll_arg(self, arg) -> List:
+ """Translate a call argument to a list of TExp (tuple/list literal,
+ constant, tuple-typed name or tuple subscript)"""
+ if isinstance(arg, (ast.Tuple, ast.List)):
+ return [self.visit(el) for el in arg.elts]
+ if isinstance(arg, ast.Name) and arg.id in self.env:
+ if self.env.has_constant(arg.id):
+ v = self.env.get_constant(arg.id)
+ vals = list(v) if isinstance(v, (list, tuple)) else [v]
+ return [self.visit(self.__py_to_ast(x)) for x in vals]
+ t = self.env[arg.id].ttype
+ if not hasattr(t, "BIT_SIZE") and get_args(t):
+ return [
+ self.visit(
+ ast.Subscript(
+ value=arg, slice=ast.Constant(value=i), ctx=ast.Load()
+ )
+ )
+ for i in range(len(get_args(t)))
+ ]
+ if (
+ isinstance(arg, ast.Subscript)
+ and isinstance(arg.value, ast.Name)
+ and isinstance(arg.slice, ast.Constant)
+ and arg.value.id in self.env
+ ):
+ t = self.env[arg.value.id].ttype
+ if not hasattr(t, "BIT_SIZE") and get_args(t):
+ inner = get_args(t)[arg.slice.value] # type: ignore
+ n = (
+ len(get_args(inner))
+ if get_args(inner)
+ else getattr(inner, "BIT_SIZE", 1)
+ )
+ return [
+ self.visit(
+ ast.Subscript(
+ value=arg, slice=ast.Constant(value=i), ctx=ast.Load()
+ )
+ )
+ for i in range(n)
+ ]
+ return [self.visit(arg)]
+
+ def __call_range(self, node: ast.Call):
+ args = [ast.literal_eval(a) for a in node.args]
+ return self.visit(ast.Tuple(elts=[ast.Constant(value=i) for i in range(*args)]))
+
+ def __call_len(self, node: ast.Call):
+ if len(node.args) != 1:
+ raise Exception(f"len() takes exactly 1 argument ({len(node.args)} given)")
+ return const_to_qtype(len(self.__unroll_arg(node.args[0])))
+
+ def __call_sum(self, node: ast.Call):
+ if len(node.args) != 1:
+ raise Exception(f"sum() takes exactly 1 argument ({len(node.args)} given)")
+
+ def rec(lst):
+ if len(lst) == 1:
+ return lst[0]
+ return lst[0][0].add(lst[0], rec(lst[1:]))
+
+ return rec(self.__unroll_arg(node.args[0]))
+
+ def __call_anyall(self, node: ast.Call):
+ if len(node.args) != 1:
+ raise Exception(
+ f"{node.func.id}() takes exactly 1 " # type: ignore
+ + f"argument ({len(node.args)} given)"
+ )
+ op = Or if node.func.id == "any" else And # type: ignore
+ return (bool, op(*[a[1] for a in self.__unroll_arg(node.args[0])]))
+
+ def __call_ord(self, node: ast.Call):
+ if len(node.args) != 1:
+ raise Exception(f"ord() takes exactly 1 argument ({len(node.args)} given)")
+ return self.visit(node.args[0])
+
+ def __call_chr(self, node: ast.Call):
+ if len(node.args) != 1:
+ raise Exception(f"chr() takes exactly 1 argument ({len(node.args)} given)")
+ return self.visit(node.args[0])
+
+ def __call_minmax(self, node: ast.Call):
+ if len(node.args) == 1:
+ args = self.__unroll_arg(node.args[0])
+ else:
+ args = [self.visit(a) for a in node.args]
+ cmp = "gt" if node.func.id == "max" else "lte" # type: ignore
+
+ def rec(lst):
+ if len(lst) == 1:
+ return lst[0]
+ conds = [getattr(lst[0][0], cmp)(lst[0], x)[1] for x in lst[1:]]
+ cond = And(*conds)
+ t, v = lst[0]
+ _, v2 = rec(lst[1:])
+ if isinstance(v, list) or isinstance(v2, list):
+ # multi-bit elements: ITE elementwise
+ va = v if isinstance(v, list) else [v] * len(v2)
+ vb = v2 if isinstance(v2, list) else [v2] * len(va)
+ return (t, [ITE(cond, a, b) for a, b in zip(va, vb)])
+ return (t, ITE(cond, v, v2))
+
+ return rec(args)
def __visit_Call_typecast(self, expr):
if len(expr.args) != 1:
@@ -402,10 +651,18 @@ def visit_Call(self, expr: ast.Call):
elif not hasattr(expr.func, "id"):
raise exceptions.ExpressionNotHandledException(expr)
+ # Builtin functions
+ elif expr.func.id in self.__call_dispatch:
+ return self.__call_dispatch[expr.func.id](expr)
+
# Typecast
elif self.env.know_type(expr.func.id):
return self.__visit_Call_typecast(expr)
+ # Print
+ elif expr.func.id == "print":
+ return [], self.env
+
# int()
elif expr.func.id == "int":
return self.__visit_Call_int(expr)
diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py
index e06b97f4..80dc7651 100644
--- a/qlasskit/ast2logic/t_statement.py
+++ b/qlasskit/ast2logic/t_statement.py
@@ -14,13 +14,42 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import ast
-from typing import List, Tuple
+import copy
+from typing import Any, List, Tuple, get_args
+from uuid import uuid4
from sympy import Symbol
from sympy.logic.boolalg import Boolean
from ..types import TType, TypeErrorException
-from . import Binding, Env, decompose_to_symbols, exceptions, translate_expression
+from . import (
+ Binding,
+ Env,
+ decompose_to_symbols,
+ exceptions,
+ flatten,
+ translate_expression,
+)
+
+
+def _py_to_ast(v) -> ast.expr:
+ """Convert a python constant (scalar/list/tuple) to an AST literal"""
+ if isinstance(v, (list, tuple)):
+ return ast.Tuple(elts=[_py_to_ast(x) for x in v])
+ return ast.Constant(value=v)
+
+
+class NameValReplacer(ast.NodeTransformer):
+ """Replace all Name with name_id with the given val"""
+
+ def __init__(self, name_id: str, val: Any):
+ self.name_id = name_id
+ self.val = val
+
+ def visit_Name(self, node):
+ if node.id == self.name_id:
+ return self.val
+ return node
def __translate_assign(
@@ -38,6 +67,40 @@ def __translate_assign(
target = stmt.targets[0].id
+ # __ prefix is reserved for internal use
+ if target.startswith("__"):
+ raise Exception("invalid name starting with __")
+
+ # track python constants for constant-subscript unrolling
+ try:
+ env.set_constant(target, ast.literal_eval(stmt.value))
+ except Exception:
+ env.remove_constant(target)
+
+ # If self reference, use a temp var a -> __a = rhs; a = __a
+ if target in env:
+ tval, val = translate_expression(stmt.value, env) # TODO: typecheck
+ old_syms = {Symbol(b) for b in env[target].bitvec}
+ # note: val may contain raw python bools (constant subscripts), which
+ # have no free_symbols
+ refs = (
+ set().union(*[getattr(v, "free_symbols", set()) for v in val])
+ if isinstance(val, list)
+ else getattr(val, "free_symbols", set())
+ )
+ if refs & old_syms:
+ tmp = f"__{target}"
+ res = decompose_to_symbols(val, tmp)
+ env.bind(Binding(tmp, tval, [x[0] for x in res]), rebind=tmp in env)
+ exps: List[Tuple[str, Boolean]] = [(Symbol(n), e) for n, e in res]
+ exps2, env = __translate_assign(
+ ast.Assign(
+ targets=stmt.targets, value=ast.Name(id=tmp, ctx=ast.Load())
+ ),
+ env,
+ )
+ return exps + exps2, env
+
tval, val = translate_expression(stmt.value, env) # TODO: typecheck
res = decompose_to_symbols(val, f"{target}")
@@ -46,6 +109,229 @@ def __translate_assign(
return res, env
+def __translate_augassign(
+ stmt: ast.AugAssign, env: Env
+) -> Tuple[List[Tuple[str, Boolean]], Env]:
+ """Translate a += x to a = a + x"""
+ if not isinstance(stmt.target, ast.Name):
+ raise exceptions.StatementNotHandledException(
+ stmt, "only name target supported"
+ )
+
+ value = ast.BinOp(left=stmt.target, op=stmt.op, right=stmt.value)
+ return __translate_assign(ast.Assign(targets=[stmt.target], value=value), env)
+
+
+def __rewrite_if(stmt: ast.If, env: Env) -> List[ast.stmt]:
+ """Rewrite if(c,t,e) into assigns with IfExp values, so: if c: d = x else: d = y becomes
+ _iftargN = c; d = x if _iftargN else d; d = d if _iftargN else y"""
+ test_name = f"_iftarg{uuid4().hex}"
+
+ def as_assign(b):
+ if isinstance(b, ast.AugAssign) and isinstance(b.target, ast.Name):
+ return ast.Assign(
+ targets=[b.target],
+ value=ast.BinOp(left=b.target, op=b.op, right=b.value),
+ )
+ return b
+
+ def check_assign(b):
+ if (
+ not isinstance(b, ast.Assign)
+ or len(b.targets) != 1
+ or not isinstance(b.targets[0], ast.Name)
+ ):
+ raise exceptions.StatementNotHandledException(
+ b, "if body only allows assigns"
+ )
+ return b.targets[0].id
+
+ if_l: List[ast.stmt] = [
+ ast.Assign(targets=[ast.Name(id=test_name, ctx=ast.Store())], value=stmt.test)
+ ]
+
+ # unroll nested for/if in the body first
+ body: List[ast.stmt] = []
+ for b in stmt.body:
+ body.extend(__unroll_stmt(b, env))
+
+ for b in body:
+ target = check_assign(as_assign(b))
+ b = as_assign(b)
+ orelse_inner = (
+ ast.Name(id=target[2:], ctx=ast.Load())
+ if target.startswith("__") and target not in env
+ else ast.Name(id=target, ctx=ast.Load())
+ )
+ if_l.append(
+ ast.Assign(
+ targets=b.targets,
+ value=ast.IfExp(
+ test=ast.Name(id=test_name, ctx=ast.Load()),
+ body=b.value,
+ orelse=orelse_inner,
+ ),
+ )
+ )
+
+ # flatten nested for/if in orelse (elif) by unrolling/rewriting first
+ orelse: List[ast.stmt] = []
+ for b in stmt.orelse:
+ orelse.extend(__unroll_stmt(b, env))
+
+ for b in orelse:
+ b = as_assign(b)
+ target = check_assign(b)
+ if target.startswith("_iftarg"):
+ if_l.append(b)
+ continue
+ orelse_inner = (
+ ast.Name(id=target[2:], ctx=ast.Load())
+ if target.startswith("__") and target not in env
+ else ast.Name(id=target, ctx=ast.Load())
+ )
+ if_l.append(
+ ast.Assign(
+ targets=b.targets,
+ value=ast.IfExp(
+ test=ast.Name(id=test_name, ctx=ast.Load()),
+ body=orelse_inner,
+ orelse=b.value,
+ ),
+ )
+ )
+
+ return if_l
+
+
+def __translate_if(
+ stmt: ast.If, env: Env, ret_type
+) -> Tuple[List[Tuple[str, Boolean]], Env]:
+ """Translate if/else by rewriting it to straight-line IfExp assigns
+ (see __rewrite_if) and translating those with the normal path"""
+ exps: List[Tuple[str, Boolean]] = []
+ for s in __rewrite_if(stmt, env):
+ e, env = translate_statement(s, env, ret_type)
+ exps.extend(e)
+ return exps, env
+
+
+def __unroll_arg(arg, env: Env) -> List:
+ """Transform a for-loop iterable to a list: Tuple literals, constants,
+ tuple-typed names (a -> a[0], a[1], ...) and tuple subscripts (a[k] ->
+ a[k][0], ...)"""
+ if isinstance(arg, (ast.Tuple, ast.List)):
+ return arg.elts
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, (ast.Tuple, ast.List)):
+ return arg.value.elts
+ if isinstance(arg, ast.Name) and arg.id in env and env.has_constant(arg.id):
+ v = env.get_constant(arg.id)
+ vals = list(v) if isinstance(v, (list, tuple)) else [v]
+ return [_py_to_ast(x) for x in vals]
+ if isinstance(arg, ast.Name) and arg.id in env:
+ t = env[arg.id].ttype
+ if not hasattr(t, "BIT_SIZE") and get_args(t):
+ return [
+ ast.Subscript(
+ value=ast.Name(id=arg.id, ctx=ast.Load()),
+ slice=ast.Constant(value=i),
+ ctx=ast.Load(),
+ )
+ for i in range(len(get_args(t)))
+ ]
+ if (
+ isinstance(arg, ast.Subscript)
+ and isinstance(arg.value, ast.Name)
+ and arg.value.id in env
+ and isinstance(arg.slice, ast.Constant)
+ ):
+ t = env[arg.value.id].ttype
+ if not hasattr(t, "BIT_SIZE") and get_args(t):
+ inner = get_args(t)[arg.slice.value] # type: ignore
+ n = (
+ len(get_args(inner))
+ if get_args(inner)
+ else getattr(inner, "BIT_SIZE", 1)
+ )
+ return [
+ ast.Subscript(
+ value=ast.Subscript(
+ value=ast.Name(id=arg.value.id, ctx=ast.Load()),
+ slice=ast.Constant(value=arg.slice.value),
+ ctx=ast.Load(),
+ ),
+ slice=ast.Constant(value=i),
+ ctx=ast.Load(),
+ )
+ for i in range(n)
+ ]
+ return [arg]
+
+
+def __unroll_for(stmt: ast.For, env: Env) -> List[ast.stmt]:
+ """Unroll a for loop into straight-line statements: per iteration,
+ assign the loop var to its constant value, then the body with the
+ loop var replaced by that constant"""
+ if not isinstance(stmt.target, ast.Name):
+ raise exceptions.StatementNotHandledException(
+ stmt, "only name target supported"
+ )
+
+ it = stmt.iter
+ if (
+ isinstance(it, ast.Call)
+ and isinstance(it.func, ast.Name)
+ and it.func.id == "range"
+ ):
+ args = []
+ for a in it.args:
+ if isinstance(a, ast.Constant):
+ args.append(a.value)
+ else:
+ # evaluate constant expression (e.g. len(a)-1) to a python int
+ _, v = translate_expression(a, env)
+ bits = v if isinstance(v, list) else [v]
+ val = 0
+ for i, b in enumerate(bits):
+ if b is True or str(b) == "True":
+ val |= 1 << i
+ args.append(val)
+ it_vals: List = list(range(*args)) # type: ignore
+ else:
+ it_vals = flatten(__unroll_arg(it, env))
+
+ rolls: List[ast.stmt] = []
+ for i in it_vals:
+ _val = (
+ i
+ if isinstance(i, (ast.Constant, ast.Subscript, ast.Tuple, ast.List))
+ else ast.Constant(value=i)
+ )
+ rolls.append(ast.Assign(targets=[stmt.target], value=_val))
+ for b in stmt.body:
+ rolls.append(NameValReplacer(stmt.target.id, _val).visit(copy.deepcopy(b)))
+ return rolls
+
+
+def __unroll_stmt(stmt, env: Env) -> List[ast.stmt]:
+ """Unroll for/if statements into straight-line statements."""
+ if isinstance(stmt, ast.For):
+ return __unroll_for(stmt, env)
+ if isinstance(stmt, ast.If):
+ return __rewrite_if(stmt, env)
+ return [stmt]
+
+
+def __translate_for(
+ stmt: ast.For, env: Env, ret_type
+) -> Tuple[List[Tuple[str, Boolean]], Env]:
+ exps: List[Tuple[str, Boolean]] = []
+ for s in __unroll_for(stmt, env):
+ e, env = translate_statement(s, env, ret_type)
+ exps.extend(e)
+ return exps, env
+
+
def __translate_return(
stmt: ast.Return, env: Env, ret_type
) -> Tuple[List[Tuple[str, Boolean]], Env]:
@@ -87,12 +373,25 @@ def translate_statement( # noqa: C901
) -> Tuple[List[Tuple[str, Boolean]], Env]:
"""Parse a statement"""
# match stmt:
- if isinstance(stmt, ast.If): # This is handled by ast2ast
- raise exceptions.StatementNotHandledException(stmt)
+ if isinstance(stmt, ast.If):
+ return __translate_if(stmt, env, ret_type)
+
+ elif isinstance(stmt, ast.AugAssign):
+ return __translate_augassign(stmt, env)
+
+ elif isinstance(stmt, ast.For):
+ return __translate_for(stmt, env, ret_type)
elif isinstance(stmt, ast.Assign):
return __translate_assign(stmt, env)
+ elif isinstance(stmt, ast.AnnAssign):
+ if stmt.value is None or not isinstance(stmt.target, ast.Name):
+ raise exceptions.StatementNotHandledException(stmt)
+ return __translate_assign(
+ ast.Assign(targets=[stmt.target], value=stmt.value), env
+ )
+
elif isinstance(stmt, ast.Return):
return __translate_return(stmt, env, ret_type)
diff --git a/qlasskit/compiler/internalcompiler.py b/qlasskit/compiler/internalcompiler.py
index 03f24d70..2e160cda 100644
--- a/qlasskit/compiler/internalcompiler.py
+++ b/qlasskit/compiler/internalcompiler.py
@@ -31,6 +31,15 @@
class InternalCompiler(Compiler):
"""InternalCompiler translating an expression list to quantum circuit"""
+ __expr_dispatch = {
+ Symbol: "compile_symbol",
+ Xor: "compile_xor",
+ Not: "compile_not",
+ And: "compile_and",
+ Or: "compile_or",
+ QuantumBooleanGate: "compile_quantum_gate",
+ }
+
def compile( # noqa: C901
self,
name: str,
@@ -74,7 +83,7 @@ def compile( # noqa: C901
return qc
- def compile_expr( # noqa: C901
+ def compile_expr(
self,
qc: QCircuitEnhanced,
expr: Boolean,
@@ -82,7 +91,6 @@ def compile_expr( # noqa: C901
sym: Symbol = None,
) -> int:
"""Compile a boolean expression, return the result qubit"""
-
# 1. If we have a constant expression, create if needed and return a constant qubit
if isinstance(expr, BooleanFalse):
if "FALSE" not in qc:
@@ -94,10 +102,6 @@ def compile_expr( # noqa: C901
qc.x(qc["TRUE"])
return qc["TRUE"]
- # 2. If expr is a symbol
- elif isinstance(expr, Symbol):
- return self.compile_symbol(qc, expr, dest, sym)
-
# 3. If expr is already been computed, return its index
elif expr in self.expqmap:
return self.expqmap[expr]
@@ -107,33 +111,18 @@ def compile_expr( # noqa: C901
# (a & b) ^ (a ^ b) & c => MCX(a,b,new), MCX(b,c,new)
- # End of speccial mappings section
-
- # 5. If expr is a XOR,
- elif isinstance(expr, Xor):
- return self.compile_xor(qc, expr, dest)
-
- # 6. If expr is a Not,
- elif isinstance(expr, Not):
- return self.compile_not(qc, expr, dest, sym)
-
- # 7a. If expr is an And
- elif isinstance(expr, And):
- return self.compile_and(qc, expr, dest)
-
- # 7b. If expr is an Or
- elif isinstance(expr, Or):
- return self.compile_or(qc, expr, dest)
-
- # 8. If expr is a Hybrid Quantum-Boolean gate
- elif isinstance(expr, QuantumBooleanGate):
- return self.compile_quantum_gate(qc, expr, dest)
-
- # 9. Other type of expressions are not allowed
else:
+ # 5. Dispatch on the expression class
+ for cls, m in self.__expr_dispatch.items():
+ if isinstance(expr, cls):
+ return getattr(self, m)(qc, expr, dest, sym) # type: ignore
+
+ # 6. Other type of expressions are not allowed
raise CompilerException(expr)
- def compile_quantum_gate(self, qc, expr, dest: Optional[int] = None) -> int:
+ def compile_quantum_gate(
+ self, qc, expr, dest: Optional[int] = None, sym=None
+ ) -> int:
# 1. Compile every argument
erets = list(map(lambda e: self.compile_expr(qc, e), expr.args)) # type: ignore
gate = expr.__class__.__name__.lower()
@@ -149,7 +138,7 @@ def compile_quantum_gate(self, qc, expr, dest: Optional[int] = None) -> int:
raise Exception(f"Unknown gate: {gate}")
- def compile_and(self, qc, expr, dest: Optional[int] = None) -> int:
+ def compile_and(self, qc, expr, dest: Optional[int] = None, sym=None) -> int:
# 1. Compile every argument
erets = list(map(lambda e: self.compile_expr(qc, e), expr.args))
@@ -171,7 +160,7 @@ def compile_and(self, qc, expr, dest: Optional[int] = None) -> int:
return dest
- def compile_or(self, qc, expr, dest: Optional[int] = None) -> int:
+ def compile_or(self, qc, expr, dest: Optional[int] = None, sym=None) -> int:
# TODO: this won't work on len(expr.args) > 2
# 1. Compile every argument
@@ -232,7 +221,7 @@ def compile_not(
return dest
def compile_xor(
- self, qc: QCircuitEnhanced, expr, dest: Optional[int] = None
+ self, qc: QCircuitEnhanced, expr, dest: Optional[int] = None, sym=None
) -> int:
# 1. Get the destination qubit
d = dest if dest is not None else qc.get_free_ancilla()
diff --git a/qlasskit/qlassfun.py b/qlasskit/qlassfun.py
index 7f04cb38..a6c75684 100644
--- a/qlasskit/qlassfun.py
+++ b/qlasskit/qlassfun.py
@@ -31,8 +31,15 @@
from sympy import Symbol
-from .ast2ast import ast2ast
-from .ast2logic import Arg, Args, BoolExpList, LogicFun, flatten, translate_ast
+from .ast2logic import (
+ Arg,
+ Args,
+ BoolExpList,
+ LogicFun,
+ flatten,
+ preprocess_ast,
+ translate_ast,
+)
from .boolopt import BoolOptimizerProfile, defaultOptimizer
from .boolopt.bool_optimizer import merge_expressions
from .boolquant import Q # noqa: F403, F401
@@ -322,7 +329,7 @@ def from_function(
def _do_translate(fun_ast, original_f) -> QlassF:
# print(ast.dump(fun_ast, indent=4))
- fun = ast2ast(fun_ast.body[0])
+ fun = preprocess_ast(fun_ast.body[0])
# print(ast.dump(fun, indent=4))
fun_name, args, fun_ret, exps = translate_ast(fun, types, defs)
diff --git a/qlasskit/tools/__init__.py b/qlasskit/tools/__init__.py
index 3abd59c6..9cc0436e 100644
--- a/qlasskit/tools/__init__.py
+++ b/qlasskit/tools/__init__.py
@@ -12,4 +12,4 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
\ No newline at end of file
+# along with this program. If not, see .
diff --git a/test/ast2logic/test_translate_arg.py b/test/ast2logic/test_translate_arg.py
index 50c8e6ff..9520f481 100644
--- a/test/ast2logic/test_translate_arg.py
+++ b/test/ast2logic/test_translate_arg.py
@@ -21,14 +21,14 @@
from parameterized import parameterized
from qlasskit import Qint2, Qint4, ast2logic, exceptions
-from qlasskit.ast2ast import ast2ast
+from qlasskit.ast2logic import preprocess_ast
from qlasskit.types import Qfixed1_3
class TestAst2Logic_translate_argument(unittest.TestCase):
def test_unknown_type(self):
f = "a: UnknownType"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
self.assertRaises(
exceptions.UnknownTypeException,
lambda ann_ast: ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a"),
@@ -37,7 +37,7 @@ def test_unknown_type(self):
def test_bool(self):
f = "a: bool"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, bool)
@@ -45,7 +45,7 @@ def test_bool(self):
@parameterized.expand(["a: Qint[2]", "a : Qint2"])
def test_qint2(self, f):
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Qint2)
@@ -53,7 +53,7 @@ def test_qint2(self, f):
@parameterized.expand(["a: Qint[4]", "a : Qint4"])
def test_qint4(self, f):
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Qint4)
@@ -61,7 +61,7 @@ def test_qint4(self, f):
def test_tuple(self):
f = "a: Tuple[bool, bool]"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[bool, bool])
@@ -69,7 +69,7 @@ def test_tuple(self):
def test_tuple_of_tuple(self):
f = "a: Tuple[Tuple[bool, bool], bool]"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[Tuple[bool, bool], bool])
@@ -77,7 +77,7 @@ def test_tuple_of_tuple(self):
def test_tuple_of_tuple2(self):
f = "a: Tuple[bool, Tuple[bool, bool]]"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[bool, Tuple[bool, bool]])
@@ -85,7 +85,7 @@ def test_tuple_of_tuple2(self):
def test_tuple_of_int2(self):
f = "a: Tuple[Qint[2], Qint[2]]"
- ann_ast = ast2ast(ast.parse(f).body[0].annotation)
+ ann_ast = preprocess_ast(ast.parse(f).body[0].annotation)
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[Qint2, Qint2])
@@ -101,7 +101,7 @@ def test_tuple_of_int2(self):
def test_list_of_int2(self):
f = "a: Qlist[Qint[2], 2]"
- ann_ast = ast2ast(ast.parse(f)).body[0].annotation
+ ann_ast = preprocess_ast(ast.parse(f)).body[0].annotation
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[Qint2, Qint2])
@@ -117,7 +117,7 @@ def test_list_of_int2(self):
def test_tuple_of_list2(self):
f = "a: Tuple[bool, Qlist[bool, 2]]"
- ann_ast = ast2ast(ast.parse(f)).body[0].annotation
+ ann_ast = preprocess_ast(ast.parse(f)).body[0].annotation
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Tuple[bool, Tuple[bool, bool]])
@@ -125,7 +125,7 @@ def test_tuple_of_list2(self):
def test_qfixed(self):
f = "a: Qfixed[1, 3]"
- ann_ast = ast2ast(ast.parse(f)).body[0].annotation
+ ann_ast = preprocess_ast(ast.parse(f)).body[0].annotation
c = ast2logic.translate_argument(ann_ast, ast2logic.Env(), "a")
self.assertEqual(c.name, "a")
self.assertEqual(c.ttype, Qfixed1_3)
diff --git a/test/test_ast2ast.py b/test/test_ast2ast.py
deleted file mode 100644
index e509d1eb..00000000
--- a/test/test_ast2ast.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright (C) 2023-2026 Davide Gessa
-# This file is part of Qlasskit
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-import ast
-import unittest
-
-from parameterized import parameterized
-
-from qlasskit.ast2ast import ASTRewriter, ConstantFolder
-
-
-class AddKind(ast.NodeTransformer):
- def visit_Constant(self, n):
- n.kind = None
- return n
-
-
-class TestASTRewriter(unittest.TestCase):
-
- def setUp(self):
- self.rewriter = ASTRewriter()
-
- @parameterized.expand(
- [
- ("a = b ** 3", "a = b * b * b"),
- ("a = b + 3", "a = b + 3"),
- ("a = b ** c", "a = b ** c"),
- ]
- )
- def test_exponentiation_transformation(self, code, expected_code):
- tree = ast.parse(code)
- new_tree = self.rewriter.visit(tree)
- expected_tree = ast.parse(expected_code)
- self.assertEqual(ast.dump(new_tree), ast.dump(expected_tree))
-
- def test_exponentiation_with_zero(self):
- code = "a = b ** 0"
- tree = ast.parse(code)
- new_tree = self.rewriter.visit(tree)
-
- self.assertTrue(len(new_tree.body), 1)
- self.assertTrue(isinstance(new_tree.body[0], ast.Assign))
- self.assertTrue(isinstance(new_tree.body[0].value, ast.Constant))
- self.assertEqual(new_tree.body[0].value.value, 1)
-
- expected_code = "a = 1"
- expected_tree = ast.parse(expected_code)
- self.assertEqual(ast.dump(new_tree), ast.dump(expected_tree))
-
-
-class TestASTConstantFolder(unittest.TestCase):
- def setUp(self):
- self.rewriter = ConstantFolder()
-
- @parameterized.expand(
- [
- ("a + (13 - 12 + 1)", "a + 2"),
- # ( "a + 13 - 12 + 1", "a + 2" ),
- ("a + len([12])", "a + 1"),
- ("if True: a \nelse: b", "a"),
- ("a if False else b", "b"),
- ]
- )
- def test_expected_code(self, code, expected_code):
- tree = ast.parse(code)
- new_tree = self.rewriter.visit(tree)
- expected_tree = ast.parse(expected_code)
- self.assertEqual(ast.dump(new_tree), ast.dump(expected_tree))