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 @@ - - - - - - + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - .py + + + + .py - - - - + + + + - - - - + + + + - - - - Simplified + + + + Boolexp - - - - AST + + + + - - - - + + + + - - - - + + + + ast2logic - - - - ast2ast + + + + - - - - + + + + - - - - + + + + Quantum - - - - Boolexp + + + + Circuit - - - - + + + + compiler - - - - + + + + - - - - ast2logic + + + + - - - - + + + + BQM - - - - + + + + + - - - - Quantum + + + + + - - - - Circuit - - - - - - compiler - - - - - - - - - - - - - - - - - - BQM - - - - - - - - - - - - - - - - - - - - to_bqm + + + + to_bqm @@ -260,4 +866,4 @@ - \ No newline at end of file + 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))