From f0e25ffafd4135b4f6995ed09365a16e0ffa86f5 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Fri, 6 Nov 2026 19:55:41 +0100 Subject: [PATCH 01/15] migrate augassign --- qlasskit/ast2ast/astrewriter.py | 42 +++++++++++++++---------------- qlasskit/ast2logic/t_statement.py | 29 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index ec6ace7a..bf13e29c 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -374,27 +374,27 @@ def visit_Assign(self, node: ast.Assign): 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 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] diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index e06b97f4..2d2aa5dd 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -46,6 +46,32 @@ def __translate_assign( return res, env +def __translate_augassign(stmt: ast.AugAssign, env: Env): + """Translate AugAssign (a += x) as a = a + x""" + if not isinstance(stmt.target, ast.Name): + raise exceptions.StatementNotHandledException( + stmt, "only name target supported" + ) + + target = stmt.target.id + value = ast.BinOp(left=stmt.target, op=stmt.op, right=stmt.value) + + # Use a temp var if the target is already defined in env + if target in env: + tmp = f"__{target}" + tval, val = translate_expression(value, env) + 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.target], value=ast.Name(id=tmp, ctx=ast.Load())), + env, + ) + return exps + exps2, env + + return __translate_assign(ast.Assign(targets=[stmt.target], value=value), env) + + def __translate_return( stmt: ast.Return, env: Env, ret_type ) -> Tuple[List[Tuple[str, Boolean]], Env]: @@ -90,6 +116,9 @@ def translate_statement( # noqa: C901 if isinstance(stmt, ast.If): # This is handled by ast2ast raise exceptions.StatementNotHandledException(stmt) + elif isinstance(stmt, ast.AugAssign): + return __translate_augassign(stmt, env) + elif isinstance(stmt, ast.Assign): return __translate_assign(stmt, env) From 39113e3644a2bf9b896aaae918bb6d25a6435ff1 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Fri, 6 Nov 2026 19:55:47 +0100 Subject: [PATCH 02/15] migrate binop pow --- qlasskit/ast2ast/astrewriter.py | 64 ++++++++++-------------------- qlasskit/ast2logic/t_expression.py | 18 +++++++++ 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index bf13e29c..701ed27d 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -374,28 +374,6 @@ def visit_Assign(self, node: ast.Assign): 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] ): @@ -553,24 +531,24 @@ def visit_Call(self, node: ast.Call): 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) + # 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/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 18a9f330..a4575c55 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -282,6 +282,24 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901 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.Pow) and hasattr(tleft[0], "mul"): + # Transform pow to multiplications + if not isinstance(expr.right, ast.Constant) and not isinstance( + expr.right.value, int + ): + raise exceptions.ExpressionNotHandledException(expr.right) + + if expr.right.value > 0: + result = tleft + for _ in range(expr.right.value - 1): + result = result[0].mul(result, tleft) + + return result + elif expr.right.value == 0: + return const_to_qtype(1) + else: + raise exceptions.ExpressionNotHandledException(expr.right) + elif isinstance(expr.op, ast.Mult) and hasattr(tleft[0], "mul"): return tleft[0].mul(tleft, tright) elif isinstance(expr.op, ast.Mod): From b18cbf4f1f2452d38e9b04a9f559bfa0e1d28f18 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Fri, 6 Nov 2026 20:02:47 +0100 Subject: [PATCH 03/15] migrate print --- qlasskit/ast2ast/astrewriter.py | 25 ------------------------- qlasskit/ast2logic/t_expression.py | 4 ++++ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index 701ed27d..7a1eeefc 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -525,30 +525,5 @@ def visit_Call(self, node: ast.Call): 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/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index a4575c55..c9288b50 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -424,6 +424,10 @@ def visit_Call(self, expr: ast.Call): 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) From cc545d51099a9bbc230724e16784ec04ffb661c2 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Sat, 7 Nov 2026 08:02:27 +0100 Subject: [PATCH 04/15] migrate if and self referecing assign --- qlasskit/ast2ast/astrewriter.py | 64 -------------- qlasskit/ast2logic/t_statement.py | 137 ++++++++++++++++++++++++++---- test/test_ast2ast.py | 81 ------------------ 3 files changed, 121 insertions(+), 161 deletions(-) delete mode 100644 test/test_ast2ast.py diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index 7a1eeefc..4006fe79 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -252,70 +252,6 @@ def visit_Name(self, node: ast.Name): 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]) diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index 2d2aa5dd..1e58e47e 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import ast from typing import List, Tuple +from uuid import uuid4 from sympy import Symbol from sympy.logic.boolalg import Boolean @@ -38,6 +39,30 @@ def __translate_assign( target = stmt.targets[0].id + # 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}") @@ -47,29 +72,109 @@ def __translate_assign( def __translate_augassign(stmt: ast.AugAssign, env: Env): - """Translate AugAssign (a += x) as a = a + x""" + """Translate AugAssign (a += x) as Assign with a BinOp (a = a + x); + the self-reference temp is handled by __translate_assign.""" if not isinstance(stmt.target, ast.Name): raise exceptions.StatementNotHandledException( stmt, "only name target supported" ) - target = stmt.target.id value = ast.BinOp(left=stmt.target, op=stmt.op, right=stmt.value) + return __translate_assign(ast.Assign(targets=[stmt.target], value=value), env) - # Use a temp var if the target is already defined in env - if target in env: - tmp = f"__{target}" - tval, val = translate_expression(value, env) - 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.target], value=ast.Name(id=tmp, ctx=ast.Load())), - 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) + ] + + for b in stmt.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, + ), + ) ) - return exps + exps2, env - return __translate_assign(ast.Assign(targets=[stmt.target], value=value), env) + # flatten elif (nested If in orelse) by rewriting it first + orelse: List[ast.stmt] = [] + for b in stmt.orelse: + if isinstance(b, ast.If): + orelse.extend(__rewrite_if(b, env)) + else: + orelse.append(b) + + 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 __translate_return( @@ -113,8 +218,8 @@ 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) 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)) From 3c738529e1cf3027474c82deb163e20784a30807 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Sat, 7 Nov 2026 08:29:11 +0100 Subject: [PATCH 05/15] list migration --- qlasskit/ast2ast/astrewriter.py | 7 ------- qlasskit/ast2logic/t_expression.py | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index 4006fe79..f6907e9e 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -138,7 +138,6 @@ class ASTRewriter(ast.NodeTransformer): 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, @@ -151,12 +150,6 @@ def __init__(self, env: Optional[Environment] = None, ret=None): "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): diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index c9288b50..2681d6a9 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -199,6 +199,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: From fc145875c3a23ab49a52bd3fbbcfb9540726c895 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Sat, 7 Nov 2026 08:36:38 +0100 Subject: [PATCH 06/15] migrate subscript --- qlasskit/ast2ast/astrewriter.py | 88 ------------------------ qlasskit/ast2logic/env.py | 16 ++++- qlasskit/ast2logic/t_expression.py | 103 ++++++++++++++++++++++++++++- qlasskit/ast2logic/t_statement.py | 13 ++-- 4 files changed, 125 insertions(+), 95 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index f6907e9e..f015dd39 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -150,94 +150,6 @@ def __init__(self, env: Optional[Environment] = None, ret=None): "max": self.__call_minmax, } - 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("__"): 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/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 2681d6a9..4818dfb5 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 @@ -52,6 +53,70 @@ def visit_Name(self, expr: ast.Name): 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 + 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): + return ast.Subscript( + value=expr.value, slice=ast.Constant(value=k), ctx=ast.Load() + ) + + chain = 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 + # Subscript: a[0][1] def visit_Subscript(self, expr: ast.Subscript): # noqa: C901 def unroll_subscripts(sub: ast.Subscript, st): @@ -73,7 +138,35 @@ 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) + etype = args[_sval.value] if args and _sval.value < len(args) else bool + esize = etype.BIT_SIZE if hasattr(etype, "BIT_SIZE") else 1 + elem = vexp[_sval.value * esize : (_sval.value + 1) * esize] + return (etype, elem if esize > 1 else elem[0]) raise exceptions.ExpressionNotHandledException(expr) if isinstance(expr.value, ast.Name): @@ -106,8 +199,16 @@ 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): diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index 1e58e47e..a76ee8cb 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -39,6 +39,12 @@ def __translate_assign( target = stmt.targets[0].id + # 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 @@ -72,8 +78,7 @@ def __translate_assign( def __translate_augassign(stmt: ast.AugAssign, env: Env): - """Translate AugAssign (a += x) as Assign with a BinOp (a = a + x); - the self-reference temp is handled by __translate_assign.""" + """Translate a += x to a = a + x""" if not isinstance(stmt.target, ast.Name): raise exceptions.StatementNotHandledException( stmt, "only name target supported" @@ -83,10 +88,8 @@ def __translate_augassign(stmt: ast.AugAssign, env: Env): 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 + """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}" From 48b9922761951af50c0b47b2eaf37a82ecc72f5f Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 14:53:48 +0100 Subject: [PATCH 07/15] migrate the for to ast2logic --- qlasskit/ast2ast/astrewriter.py | 124 -------------------------- qlasskit/ast2logic/t_expression.py | 4 + qlasskit/ast2logic/t_statement.py | 134 +++++++++++++++++++++++++++-- 3 files changed, 130 insertions(+), 132 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index f015dd39..02ff2030 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -23,87 +23,6 @@ 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""" @@ -116,20 +35,6 @@ def visit_Name(self, node: ast.Name): 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""" @@ -173,8 +78,6 @@ 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): @@ -264,33 +167,6 @@ def __unroll_arg( 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) diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 4818dfb5..8e8abd7d 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -169,6 +169,10 @@ def unroll_subscripts(sub: ast.Subscript, st): 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]) + if isinstance(expr.value, ast.Name): sn = f"{expr.value.id}.{safe_str(_sval.value)}" else: diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index a76ee8cb..0681a75d 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -14,14 +14,35 @@ # 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( @@ -116,7 +137,12 @@ def check_assign(b): 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 = ( @@ -135,13 +161,10 @@ def check_assign(b): ) ) - # flatten elif (nested If in orelse) by rewriting it first + # flatten nested for/if in orelse (elif) by unrolling/rewriting first orelse: List[ast.stmt] = [] for b in stmt.orelse: - if isinstance(b, ast.If): - orelse.extend(__rewrite_if(b, env)) - else: - orelse.append(b) + orelse.extend(__unroll_stmt(b, env)) for b in orelse: b = as_assign(b) @@ -172,7 +195,7 @@ 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.""" + (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) @@ -180,6 +203,98 @@ def __translate_if( 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): + return arg.elts + if isinstance(arg, ast.Constant) and isinstance(arg.value, ast.Tuple): + 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] + 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 = [ast.literal_eval(a) for a in it.args] + it_vals: List = list(range(*args)) + 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)) 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]: @@ -227,6 +342,9 @@ def translate_statement( # noqa: C901 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) From e9c828cded198bb145804f7d415825df34302b74 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 15:00:00 +0100 Subject: [PATCH 08/15] type fixes --- qlasskit/ast2ast/astrewriter.py | 4 +--- qlasskit/ast2logic/t_expression.py | 38 ++++++++++++++++-------------- qlasskit/ast2logic/t_statement.py | 37 ++++++++++++++++++++++------- qlasskit/tools/__init__.py | 2 +- 4 files changed, 50 insertions(+), 31 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index 02ff2030..c2249800 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -14,11 +14,9 @@ # 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 typing import Optional, Union -from ..ast2logic import flatten from .constantfolder import ConstantFolder from .env import Environment diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 8e8abd7d..4235d6af 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -86,7 +86,7 @@ def __const_chain(self, node, slices) -> ast.expr: 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 + return ttype.BIT_SIZE # type: ignore args = get_args(ttype) return len(args) if args else 1 @@ -99,12 +99,12 @@ def __dyn_subscript_chain(self, expr: ast.Subscript) -> ast.IfExp: vt = self.visit(expr.value)[0] n = self.__first_dim(vt) - def access(k): + def access(k) -> ast.Subscript: return ast.Subscript( value=expr.value, slice=ast.Constant(value=k), ctx=ast.Load() ) - chain = access(n - 1) + chain: ast.IfExp | ast.Subscript = access(n - 1) for k in reversed(range(n - 1)): chain = ast.IfExp( test=ast.Compare( @@ -115,7 +115,7 @@ def access(k): body=access(k), orelse=chain, ) - return chain + return chain # type: ignore # Subscript: a[0][1] def visit_Subscript(self, expr: ast.Subscript): # noqa: C901 @@ -147,31 +147,35 @@ def unroll_subscripts(sub: ast.Subscript, st): 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)) + 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) + 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) - etype = args[_sval.value] if args and _sval.value < len(args) else bool + 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] + 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]) + 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)}" @@ -208,9 +212,7 @@ def unroll_subscripts(sub: ast.Subscript, st): else: # 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 + ".") + b for b in self.env[sn.split(".")[0]].bitvec if b.startswith(sn + ".") ] return (inner_type, [Symbol(b) for b in bits]) @@ -395,17 +397,17 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901 elif 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 + expr.right.value, int # type: ignore ): raise exceptions.ExpressionNotHandledException(expr.right) - if expr.right.value > 0: + if expr.right.value > 0: # type: ignore result = tleft - for _ in range(expr.right.value - 1): + for _ in range(expr.right.value - 1): # type: ignore result = result[0].mul(result, tleft) return result - elif expr.right.value == 0: + elif expr.right.value == 0: # type: ignore return const_to_qtype(1) else: raise exceptions.ExpressionNotHandledException(expr.right) diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index 0681a75d..cafbf775 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -22,7 +22,14 @@ from sympy.logic.boolalg import Boolean from ..types import TType, TypeErrorException -from . import Binding, Env, decompose_to_symbols, exceptions, flatten, translate_expression +from . import ( + Binding, + Env, + decompose_to_symbols, + exceptions, + flatten, + translate_expression, +) def _py_to_ast(v) -> ast.expr: @@ -98,7 +105,9 @@ def __translate_assign( return res, env -def __translate_augassign(stmt: ast.AugAssign, env: 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( @@ -234,8 +243,12 @@ def __unroll_arg(arg, env: Env) -> List: ): t = env[arg.value.id].ttype if not hasattr(t, "BIT_SIZE") and get_args(t): - inner = get_args(t)[arg.slice.value] - n = len(get_args(inner)) if get_args(inner) else getattr(inner, "BIT_SIZE", 1) + 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( @@ -252,11 +265,13 @@ def __unroll_arg(arg, env: Env) -> List: 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 + """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") + raise exceptions.StatementNotHandledException( + stmt, "only name target supported" + ) it = stmt.iter if ( @@ -271,7 +286,9 @@ def __unroll_for(stmt: ast.For, env: Env) -> List[ast.stmt]: rolls: List[ast.stmt] = [] for i in it_vals: - _val = i if isinstance(i, (ast.Constant, ast.Subscript)) else ast.Constant(value=i) + _val = ( + i if isinstance(i, (ast.Constant, ast.Subscript)) 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))) @@ -287,7 +304,9 @@ def __unroll_stmt(stmt, env: Env) -> List[ast.stmt]: return [stmt] -def __translate_for(stmt: ast.For, env: Env, ret_type) -> Tuple[List[Tuple[str, Boolean]], Env]: +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) 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 . From 04dd3d0579b37fca7448c9bce742828d4da6672c Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 15:03:42 +0100 Subject: [PATCH 09/15] remove ast2ast to docs --- .../qlasskit_transformations.odg | Bin 16844 -> 15732 bytes .../qlasskit_transformations.png | Bin 22870 -> 18723 bytes .../qlasskit_transformations.svg | 1004 +++++++++++++---- docs/source/how_it_works.ipynb | 5 +- 4 files changed, 807 insertions(+), 202 deletions(-) diff --git a/docs/source/_images/trasformations/qlasskit_transformations.odg b/docs/source/_images/trasformations/qlasskit_transformations.odg index 63386e5e9e8ff3480a5c7c4d44dfde1cdbfb5955..c190a79195ab0ac396feeb169b49a774e613824f 100644 GIT binary patch literal 15732 zcmc(GWpEtXvSte`W{a7bnPrQaEM{hAW=4xGX117F7Be$5gT*YZd|%9Z?(EEcyFXrR zMpblHM}J?PlT}@Hl4K=7K#>6e2mnCB##}QKgtmtY008{j-@gJ_npzqgzg~T3gXMSsBn;>)M;z(^}gYS{YdDJ6alAInWy0>Dri>>f6iy8{s?0{}%LjO~Bg9 z$kf=;?q5{)jCAi%M?GCTx_?Gt{te2;+Q!l57xX{i@css8YN=~%Xivv)>R_pBWB(t( z*nb1o)z>$)Fnk}m^?w@f-yv=6tc~pq?d|{HCu;AY>)`m`;Qr36Mi$n(4u=0}oPTFl z2Wx8!8(k|yi~nGmzdPG6-0!(TKtTNKUV1m-{}!ls&ECP)!qA@9#nNI`V>x!Y5z%v8 zxu~|zb^B+|uxOZZeXG-{6_U1mJ)5HIlAow(PB5_wK6A>2%nrj_5*;Kan;){69HwfO zv4&ylC${D%huqK1zOEP30x2+O3^aPNt1vHA*6f|CqB-p9>oDGL*WpZ9uMSUzXa;p* z0<9sUjZ(IKX?{GUjp#Oe-&L}xrLBY1Q)nP4xAbDNk1<~Fh*Np4m-CG4{VEkSl3Hs+y=mEZ}bwKGbp#FNFy^Am13{39-_R zss?9#OxjA5HA=JXI6Dn{avYX~U<YCa$*~I1J(S?U8= zSNOzt;Litq9-aAsK6Fh^dkMcNsC>38_db(v7TbJ8yVDQ&M* zo1#xOsm8(&cdc0~Xj;s_;d#Ri6LdYUA&7MH{zIx}GLl)RJrSLT{F5sj&O)>jmy8=RH5B60ps=|2fzU4Bha_8h#{uB0gMKo6Y zcFqwheG+={*By2`_w@$@+6m} zC45^RGAX2Jz~$+9z*85Ni+?9p1oz_D-MY2sS|U3yvlZ|;G{ga>jlY~iCo?Rjv56QH zM2aDJqJ@jN$N1(b?{i1T^(4VzZlNe0I*IeMA|?u!df@ysMnDHC%qs@l5kL?`ngRV> z25$6|$|m$%Qna*R4NH7h8xpO;!u#s}Ri(x|EzF0!#>hv6|XY&TTTjj~3t zjm^nO5c8~XpN8evvE_)bi%bZbvpA2kXo!TOI%n9s1Y>iE*YSBg+DG~8QQQp)60Hli zn1kgDPlD^ZROKViT;R7`XbK9k1tb_`aG=L3n<2w%AOu383u@6J91t_pl*lP6edXa5 zJ0mO`M!x!+x`SCmpPV!bR@(>I1Ty7T!hWUvhyV`>Kb4+utnTXDB7BW@fwqPhVNbe* zB)VcUn!1smz!qST$}rUb^0K|w2uGpDI;2BshXX9~Oj-#PIa7FgMTJLMWeA=8OJlTidtgQbAbozfBlvFk3y7KC zV?h)5(7udxnLjR4++VNibuNzN0(7DX@4p@YR0S=${{b3b#hDnuaNo5sleB^I5p0Ib zq;*Zfow8VbkK!;9HmnD{hCOU3JVb*@Ye3ortu)XjUK7rvtr6q(j_-|C*92BiC}%@#dqiv^_U;?Rx213~ z9;ri%9HLhqnMgdmX%ev@Pk9vy#Lqv8^naXyAHuSbSyMn*bp_65`E^c54~HTPBRc{*&) z;9WvdnN?l1{c^p2Itv7C>bdWnPUVVv(W$@L%u|?I=}L>j@<_c9OE_Z}n1q*V!%i7+ zlL|5>@!@C5-eBFVF$gI+`KT`Pb(-Pn7>q$1x;*t#Xg$Gd!>qbRU^UGy%=&DDmNoX0 zdqas;ve*Qf)Pj6Jf+Lt4h(|XOHnYjSafLF$deh+&*<+sVj9&FKF-mA*R#Ladbh{2* z87Z-md*S8Hr!&d;Z+YfRB@DMssXQ$xlau?_%=*i&psU(BbMg?eKNr*W5=+)fR#Ay@ zKxeCE8yjQIG_Sk!2XBU;5mVUf@soLu(7($Y|-S>daY&MRM^{PYM9nn8lT$KCWZ%hHD* z8^}*uQPI!`8Ml%B{R?+k!m?0j)v_@j60Be%ix9puhx}{a6vuATXy>WPwA&vG!{<3P zr7O?#?-cn>d3hY0YgJ4Nh8y&^>%3X+?xXo2No$T5pYsN%_hcDdY4y$dBE8GPc%44* zRT3u!%ec`Ft}m3Au|+(pOjmim+zhbgwA4Lc8 zVtY|zicfoep6|BH?~idy0xwA|u-O&7sRE-6d_-Tc2%jt)PQ4+rsf&#$LQeMy|?8U%ZnI! zsnqwG`C2NmCVMi41QwNSckp8jxit^AApbft2Nsg%;iQkPncs=O$T+8t>+}Y`@S{^- z9KBojO?&6DN6QbHv4AZ}uEu*Q^HbHYFl%WFMu`BN>SbV`vYWSIU?<0)oDEe zr!m&>VM!6N6@mrj^)k(I#b}#a9Isy4g9Es-UQ8|x?`lI7 z^S6t7YBCk(QM|GniY3#DOO{77-n&xGN0`%lx0UCzElVFk#lJ%_^Hk?gQ~WM zbj0Ais`OSIg^VIO65D^t%E|_nLSK`4dQ3WHW=~;Q(J|8iANdTYTpJhUam+6^^*KgQ z!D7=~k8~KpY84JLj1iT-+~u&2TT)xPj8SLUrv7Y%J<g_4AyTH21aYB9pV ziMqnUeP7fTW)*&^tPnSi;<;f0$BLz~1b+M7{gLuW>iYsXYs(DZT}7V^%mW;X9d$d-QWyY0 zNd^G;FBkFeybI`e??T_&%HfwA`tDww{@J}Kwn%UV5|hi@N-kR{=O%Wl#*&eDwf{^^ z{DTw{4AgNkbD{sG!$S=4lTa3kBq~qNC@+~ZnmVgxlHKz&2u+*xBPJ zwkMu=P~x7!>+-kpU_NuLxBjQ~Hj+3Mt!9u%_rXukoQowrHrPe z8emLay)ll+C+b$AMt$s>gG|SSf1?-7?3;+iK)Hzz(ya|&Vw76`hDl%`o2p-XU%sU> zrqUV*a^G$}u$IEyO_Sq$aV~?$2hM~n08579FXl6+v8h|0;=*r|RyUz8JgJEoOl*}f zLcz?u?=v@}`pI^PQ(alUGzdY!c^9>olJ2fn6}m6uBGh?EJCj%GIxy84rwj(lkegqW zCryW(oYDb1_r5(tbv`ChHBU-3eE#Y)n!AXm>&75gYL@CjX0HG7q`)tE7a#T(F zSc}}C0O2X!(xRF&lB%K6xkqdRBP^mgF=w=G4<#Pbor3jq{dR&-1!yN6iHXbF3QA0Q zu9GpVU`jfvMO+?mFd>R;$;eAX4h5;!;evn=WPvV78JCq(u_dI(MUSP$`YTfjlMhUy zT#6^U0+_*$bi@x`mO(;em zBXTNJwGn8>Skw7!(6+>-^Ji=y?-Vt^eCJG-W0Q38%$3sdsYUo8=k!)D``zjcWjzqr z`DML3-oX1gArkqc$&kM6oM9iQwO5(Es@d}4Tbczr;g{kyN4IuGV!i(c2yp7c4)O#WX7dSjYT+n z;T~3+iM%vUprLixn{MP8{{-=*z<|WAV#X)SIf_-ibop;%HnJ>*U&B@lo^wzn_2NGA zW8P9Vk%T8+)<%o8?^)41KLNNLyq|J zkl#|6{53E{NXj2E#j18>@c20p|&s~S|GuF(;<9Z94RVB|m$4HoNO2x>zr^?`b#(C60dA zw*}AhqwtAXGe{`}X z7{w4+UqwHPCG+#EG^uOoxVmKXhGMl5hfYD3PFdihE@g+%m-eq{abK^56!f?Izsf){pBj?}xs1(h2Xn|K$Pypm{K6+^6@uZXOA4h`+96i8cD!&C`ZG~P zBc~5k1bZy)Bj!^R|JAfodBsbdOasC?>#9{D?-Nd@NX)mLzP;R*NA<&#sv|u1QZKdB z2H;zbrKHfhIW4nA1rH{0wC)&Wuy3|fKBivUresa72qBurySipcCi4psu^yaM{6%Zk z^$1mp&7X-fiyRx#I$4tPFy5U*U^+u_br$AG-I(4cQ`ekqk_ACaxeN;` z;8?Y8XBY)>OuUrMZ41B$>(=-)7DdjJI^nyKt1$Wwbd+71Gu*Jm_o} z*``G{bkcnU62pe`^qC2e74bFj_-_{xfSVl?AUKQ#2U^4fz>Eb3rqW9V2Y9gfZxy$g zxbb+EQwrN^2zE3~Gj7L2CLH%nZ3T8D5n#2Ha+I2f)s{^>d5TWZ<7^bYDb_)ny{TmU zdX_)+CdR0^X(dV!^qzNuD`N3ph~%ocIG~DqM;HGYt&l0B)Q{D-a*wRwzwM(teyc!+h_DL3@6ABXFTY z@OH)7X+;bNzXGVv+LT+hjH--#T|S!(PGp~a_wrQ-P^=XtYHlY!jh zAe%JqiBCRMp8U+MM8y2V*9##C`ywqqHtN1MOTH?=au5ggHE_=OMhEWZ5Y&es%9}{5 z0
    idZE}6*(_h2bT@Oapuu4*VdOAJ$Ox9gCBE{_yz8PD&lsyUGwgQ19 zaeY7Fn-gf8Yib7HSJURR9;<6bR~=1AQaM&u4D*1E8k<*o%a|uq5-ebMiBVmi_WTm5 zoCUJnR`vZwlHMc|@-6TCDe%6|+^yqs9VUdz7=`N7J~k<7v2KdrwiO^{Qi??*;;F>W`m*ZHho2MruP^^7ctfA2_^{)SBTD+cM2(=>8T)^p< zhvk_1RH63;DKZ>Zg*#uJ`Mf!ufnb*Idb>eT=%cSTn|)$Yeoo*qQ|idzQBswB_X&=2 z&H}bltm84s7ylBFjAZ<#VpBkxA`y=;G5G(67bf&pGADYe)bfSLr`37T;Hs0DD6R2U9EK{|ZK{ zsoQU{B72=zEa)yJJ8`Pwe?3WA%+sfZGw}6c2>3aLSl_8-Fi14Wa8hv|!nTE+C5_wC6dGzSY@c;LH%s|G)-2o;=)?a zF#_i)xf|Fi--U1iSMWt{nb#;{6?5dA zk#FI5NqvHG69SjR&2F}53%AU(yg}D`p_2o&+GxE8U|tV6rlk1kw6(R(k~fzZ)SE+> zl7?ra6B@R(@5vi~7WvOFaKs1Z?&$Q|0v1k=rw>MqY$=H27bs0wJILUvc+@2nm%9s*h{=JQag8xS zm~k#geBl!#by82e^0gT$NhPUVJim6XH8DXX#OdN72iC$P_+E75*bFCrBelzttGP*x zOdpUhlhc~-klfZa7A=JA4Kr)p9N@+?-o5ELrxVbo#Nck1Lh$1`7)#L2Ls6ZtyWO?1 zrYS<8q;TX4kyO^VGPiRaY6a1j^7>nxKNV-rAis^Bzsv1Vm-(VyYi_C$Dnpe|Q1}5- zs>N{kIGvFoH+VSE9%*>2zO0lg?GGwQpb!cDp~}~DS38mxed(*6_$r0gvL6U&_l>tn zc=D8QIL`rR$rG>}<{~H+d69>>4~=ulStw%R2^qK9mb;>iUS8uKXj1`aoCVl}dW^(l zY#1_cK?E`n=u?Oc<6rdD6eg2%Jte{1vL@`a;+XoL!BZ*+GX#v&mnx-nL@Wc~$G_1* zgg3i{o=CxU)px1~Riw~^EX{^oD8l7nM#$rnMMMi>JAQ26nQWw{aH)U`bkCK;OS58mdzN{K;xdt6spDu8Ka=7m`goBMIBRmCH{I7r)ZuNq6~HctGFl zT1I+Rn3eDze3o$6R0^I5(T=;IB4jh_Oa2O)+ADUq)*(1(g$U~%aIw-S+(H~)g>ayL z=n-8@Ht=+&>c)i-+JNvqGk)ql-&k0KR(pBjpjl+-$-v;ty8Pj<`g%L`^kmT~2<)PI z{!Hs#U)62WMYygo>@i!+5aiW^$exm6`d|=5B%*+wksrOZ_?JYN%JW@17fx_FPG278 zIYegU_TdT0I#2?`*=fOK9rAiQ6pr}bFrh}gmj~&fApH@Y*P!Ake_h1V0RnXn~b1q8WW8*{2>SP$kns&(;{g;Mmhwa8R`-1ep;Rh~HYsea1fEQ&nH zBlvWOT9{fUnWB#SgQM;v z7i19G)seCvegSA}DDjZxYAsLy58L5{;nE^aWII=$Jk(u-C&Fi6R)~+T5XJ6D%|0ur zd)#sQsbmncsCqe-4CkNrbG;$~ffQATOX~2R*B?&sBLc#@4H!jtb@?)rLb-Cm$U8L< z2M`lCHwRA^L!7jzd2it@1ayG6J2A#xtY!oW`7#a+!c_H&UT=A=avQwq5?3~BYn@Jq z2aR@Q#HZy=S2LF$y)Ud)GGO|({l^w2++?PCtCWFkVanEPNE7_Cqnp>J7j5|DtPh*H z{7Mr+@QAdpM!0gXnWyGq)z-86OQ$TBCWVDPbyqR8ji;Z+Yl@pHzbh?<`WY{i8Awab zHxZ3r(d}-w+G$sN64_jD47bX|8%<}-VRhZ?s&Pw6F}kERT{&ekKUbSSNz>m&qU6L? zjA>%n1gTXZ3atj=IHo@C7fMJ0*34MTl4NUY504q(2AD&)pXxty8B|}XC+tHn+q^P-8^45_$x(Us$BaL$#PiurW|6V6qQ{J7BA(GX%95qe9HofMgw!|d$>9PxP_U{$M$&89;d$Wkpjhc*HhL(m-^{Yc?Is&4%b|pVWIW(9ZQI^DSYqu9T??BffUFC2B8HjLDyIA6?T=dlXN18g zms1XcZl=JN$a%cT)!VIOdB&dXBp2UcJ{}1c4LZKK1EddQK(>c&FY5Hrwqfk~$Pc^B&-i^PdJH@y z6i9}G-3W@@a4J2eWkJds?T|_pi%s zFq;S(ju@> zn7=ajuwtTu@&Ewvy9#6m0rLK#PJm?k{zap#q=L{FXh?Ykc*T!MYD|nqKmY?+XwwfM zjz9os7$`4zSTB^1-sq_AnCSW>#1?$K4$Mq0?;AUtsg$Ijwx>7bI9WEYuJ<=pYPa4H-dCA3n|iKF$mQ?g~C`KN0@W`$j=JOiek$ zKs(CJILgjC#?3K7OfXAMvP43-L_xYtLp8z6y~;?x#=*8lgttY6zsbit@rid*h<{2{ zcv@0?Mpk-8L2gz>X--3JMN4B|M{_|}YffKh(NKTM)Og9lY{kZE#lddX#c9pmZ9_z8 zLri2#QhZxhdRIYyS50+KU-wW~XT#g;)YWCp$7{paJIco=#@|07A|fd^HX}K?t);~` zvoEY*D5i8Yqi#B@VJ5d}wxoTbtaG`#XSrcut$Apod1$?^f3tODW8L3(BfxJnBxoxl zZ2N1}PJG<%x5T~FleI%C)0UQ}va;K%s^{9; zr+&;QIJi9)RzPfq1cz%Amxqi66eSLj>zk%Q0-ZqP# z^xri|8&Oqz008>quN?@GlJ@?mK*|ym{H)-zbebM%o!al&&jd59M5JC*w{SGqyHq}0 z!mL>!W!E|vD#0@u=Pu1l9OeT~8I3G7P+6r{V5%x?LB+^cF_xldD#tt)&cLjs8dQuN zWQf^aq~D*AtCVU$>Xp%_NGy=EcJF<1=6dEDL(P@rmyPi7!j1IsQ^?I?b>Hi*Wj%ej zr6eo637QWW0)P+j`St(%Zxof$Ug*?)X9S($tJIeEshR790`Q(f`!C&tAl}cW5S6=4 z&$HH!?hdmKQ*|;=oS+Mjz$@L7(|KF;Qr%f=#qJ{K4*ageZ1|sWASNvBY`?AGw-pyz zrO1t8>|b)6>DZaE?`IpY1S4c=fo5&t4-Q`Dd5&!eI#^wpn)K}S;A675MVZFih@+DP z2^SmLm@!_Ew0s4b2?QwI8V{Xf=Y#JvKE~h@@pcYo$AJXJoSou@VYyPJi_7sMNEE=q zUi1vc(|-cZeOjtp1_RhA7AzhpN|Eq;0in+_fVXkhZvWN;3uTW#fxLlp`5pullMP)tGv zvh3SrTCFZwZ0F-3km#F-LT=fj*r{7{-Fm_8C-bE!WUrjwlY22lAT7&PcOnYVgQ{4| z-iWbX1)v%qn!&EF#~Pm;maij9@|O9NJ6N|`RB@C_TY=vZvWzF9(re?A&Gcji0bJP$ zjB4?EsFfd5*B-uU(Sai%P1wf`=h}9~_I6#Q#n46(Yx8h8@b44xROuz|ipv;#;=8K7Mbux3#pD)T|VX=yb&LS#h&N#^;4>Jf~k>1DbQ4z0K=qU|tVu+7?!xayJ_ z0jZm8B++zKRvx&>meH;`uP4_i8!3QpNb8D#0TG@X7AMK?34uAHAMvcTm`)J?-0IhH z+fI5|)%Gx(eqMyPI(&jI2mXBbw%qZ2ayu^52+WSS%3<*FxOzci8*+9^vW``%58XDC z5l0t&eKACABd{f<-ZDyN#ruwk#pRYP`gH2eHL1OHZ&`B(&#B_@S?dk{!&?&X9mnD; zC8WaXQN+pOr2z#F#tF`{%q~F zRcyQ#PIn24#$o1!tgYiZ3$eL}$N`%lm2ojKX?+ki@TMS!ZE3vntn7E1rW4{>VSw5_ z_~}ppmKJZ@4DwO(%y4UKuygaSG^mQotuSErQR1V%vawa0G&%4<1qsL%7(?A_hP}K` zedg{md9+$Ep~%pV+v5Q8rr8xB4(eoO^&9Yn6m!Q&nprsD&Y`93&`49#k@JmU_c)X3 z3oKAg$>s5msRW)D&XbL8+i@eJPZNW<@YiB_LS7XyqW}PS-8QR?8qaRw%{Hv&XD1 zR9<#<5H_~z4VY%V+#1s?qoxK8oMdb}7%TA0W=d9%LTZRWeus+@qED!2_$eU(#&+^_ z1D3D-kTFaX-&`_!*bzk{dUlTAYvlN7lRcDkNqxPN+VjDikIgL7Gbr5bGZA!LEd%S- zG#g*2J8vTLt{y!49?qoIL+csfq6XRzp2bG8IQO*%BLQ%Pr&yV7_IZfFveR$Eq~XF8 zk#@#g;=8xj7k(ano|?R)MIN7Ffg&-1$l+0hR-Mr9z(}kHHOf=U&XN_ZT5kzE{hkhq z52x2;+crEw9RVy}2i>=_MrKnRoD2>_p*xi%1!f9tJG(nVjt^mlzF~mykIHN54+GhI z8J?}3|8scs>rB=jVD312@~L8Jeyj-y2rUX z(tOzkeAL%}tBKLrj!wZ_As*F!a?mIBHOo`R>Ll%P6`@U*O2!vmHq&#ZZW1Jqc)*dOM3wmP?W%;R$F90wxWRey(-vg1nl zZsaz^A+_tn2prMDT~*aF-UZ*@VVd!z3nBv8^=XKw3@0l#qm0GlF{%2N!0h*ZB^+4` zRK-EfTdejrmU8Mr!qE&tfjzzuc-Q7BnWCp3AU>S^Cw}DA2d{YAbAnP}QZJ9T!z9JY4cvo9Qp-f+W6FM@8k- zbQRx2P_^ZRQVm%^SMHwu*gyy95(NO0WrcPXNQ~T^+~NbW7d|L8B3DhDYxO_O)VNMG zl3kMQg8(r=5p?`MZxs+5r;$1((jrp)Q}x;D=4~33oZ~ECk_*?TU9yYbpJ$DnF z*P0kvS)Fb!@9zT)jEt0Qz6kr0%<5>rmeM`nC*8GN*~cWBFX{!D%!U3at{1hvKX9aB zOySbOQ1UL#ARWAf49`paSSF3QzGDVqQ{c;_uU~`rfcCgaG;W!32o?pvpdO8Wt}uC|QZ8>LyuVniG1f+Hy`Bp9W2&Hz&UD=jWQs-WsNJr~ zd^qknhqCh-8ZdEh2iXV2KXDcBMDrNk6r7wWF!a^23A6Bt{5Cj0Wi|tWFaqqS7wvV=xfy^_;5e{hBuo4=3=C@> z-ex_y_h@;!^>m5!_`qbIQw2@`^z?i9`Rp==est*gZ4~YP>*S1r$dguID_?9X$o=NS z7}1Yz8tGouD7G`QxL#Z)7mug$Gs{RUpo1k)8Dbz)iD%+SAg5m}fF)^b7*g?wNuK2SF&e7YMJ z!ZX%t-!#G=8Ry#pv6b7(C?e5!76>tUL+L25WVYR%KaK`kbo%}KsVGn4SrB9 zMA9*wTtwJO%GeeCd3N%M=;Mmz*|v77DaT`8GHb%rN}dz_0}^`+zj6AqAsIM?AwX}1Af* z`fy%PX390ZacsZ^!M-yvEQx8Q`wB}%OEDr*C&&}4D%;_)dr{E)I*%-41*^b6FnR*| zyZ-6qjZ911F*VId$K9U$ZBEOO!bMl-R?oscS_9wtJWwm~)ZxwyrJ)FAKLpLvL(GFN zPy3~P*Wf2dS>>kB`tU#;MQentZ$mIsr=qo!FOJED0gnb8*z8#Zm(+_ z!fUS&nO=O?k%=W8Jl?M_rfeU--H7<1)t@iC=13wc5Rj|1vz8aCtJn03&Jn7qSI0&r zRgh6&q~j4wGooYOVaod)Mprppj;hU#C-H)rcg#OOStssMN^EXTtXr&CZC`EwK-hRA zk~LE4F7_P+D&sD)EsxxJ*fC*^r5w_R$z#oPon8qV0jkHOI=X7;a5-LzB=H_!+)B6z zO0j9#lHv9((3jJ9Ny5i@RySEVowsHI^d)&6=)$Ecc*cA1`V z6W>%~QKfDSh)Jk;in@bI$%N8?$@@Au0Rjr>NH1XFW6Fm{ecx{%c22lMrI!{@G?b3( zwp!h)>+aho2phPdKKZ!m+)#hQKH(bYeL5*7Yv#n`yIG=p#63OXT%C0_d)qF2HMEum zvvY3Z_i0ARY8uyWaX3?PZ@D!A*Z(e||D^)QQ1di`r#{Kif!>;BN&#yV{}T^Bn!g_j zep43m?qm@K=+_QbYOYVKy|hhdI~6|f>!2I|2J{^oK#fkWFMC`#u$=KzXaggLT4Vsg z5ebMNI)%D6g90lV4TO4(OR?t`hJuEpUKSMM|jzuomb~J3f>TD>phYKsSg$DXZFGIwfx~V;$k; z9FlK$tHp0>QNJdov)_KkYdZk%NQQUoSZ421!G3!3op>QDz(gdyKJ56_e6Sk z??3$RQ#08Yu3Np-+uy@?RkV6uO!Z1oApkzThr|G&fd3KYzw!3}Zz2N!e}D5oWS9Rr zi~p7Rzm2&3f3fcZ@f$eTIB7Ai{JTT;E@DE`g5~@=e*at``5x*8NC_!?rV*1CrnA(w zGBq-^|5vGGS)7zvH$6h&9Y^5gn!I{5IiWPFZw5Y(4`p;3kSX!?$Jk|MR@*IkU|54f zUEy{2tFLp*aW$iedi*jt?z?o~fu`C;Fgb7Q-YobQoNCm#giP=aIq*{4qNnOkR6>c{ z#J>?!fQuQ?cmj^vXyM3**RSm4o0XtJ&QJX6aiy8q;OxLKw2PFRq_qP!kny(`hjw_ zO%tSDIo<6;5RSo(!@;ZU3L8bO*v`WM2&}WcfA6#W zXQqEQl+E8T{kiY*pLu?zPku|$-|+nI!~AEKe;>`--?03-FY}*y{(Us4@7bEa+UIwl z=6|vLQA_>jD(heQp5G$*&hoo&^N(Bj58R&%ZhqBZ|CSQCf2+v;EAY?B$Y1q1zh(Y? z7JshJ`783zd97c`%HMMRe%rn`EB`BX`3LHsa$dht+XVld0sDjHkLmvT8Sv|g`dfzC c{^9v5EAiny=L&dVA0Pmz-cxZ*9KVkK7tK3xDF6Tf literal 16844 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⁡DUccpWcowGcw=~@iVHx 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 - - - - - + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - .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", From 359ad37659d320b2e78c34e19de714ae38380aaf Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 17:28:38 +0100 Subject: [PATCH 10/15] migrate call handling from ast2ast to ast2logic --- qlasskit/ast2ast/astrewriter.py | 138 ----------------------------- qlasskit/ast2logic/t_expression.py | 120 +++++++++++++++++++++++++ qlasskit/ast2logic/t_statement.py | 14 ++- 3 files changed, 133 insertions(+), 139 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index c2249800..16bdd823 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -41,17 +41,6 @@ class ASTRewriter(ast.NodeTransformer): def __init__(self, env: Optional[Environment] = None, ret=None): self.env = Environment() if env is None else env self.ret = None - 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, - } def visit_Name(self, node: ast.Name): # __ prefix is reserved for internal use @@ -115,130 +104,3 @@ def visit_Assign(self, node: ast.Assign): node.value = self.visit(node.value) return node - - 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 __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 - - handler = self.__call_dispatch.get(node.func.id) - return handler(node) if handler is not None else node diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 4235d6af..9098fb34 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -47,6 +47,17 @@ 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, + } # Name reference def visit_Name(self, expr: ast.Name): @@ -439,6 +450,111 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901 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] + 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 argument ({len(node.args)} given)") + op = Or if node.func.id == "any" else And + 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" + + 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: raise Exception( @@ -532,6 +648,10 @@ 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) diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index cafbf775..5698008d 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -279,7 +279,19 @@ def __unroll_for(stmt: ast.For, env: Env) -> List[ast.stmt]: and isinstance(it.func, ast.Name) and it.func.id == "range" ): - args = [ast.literal_eval(a) for a in it.args] + 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)) else: it_vals = flatten(__unroll_arg(it, env)) From 4a0e51a93e64263aade12828d679ad343e35f851 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 17:38:21 +0100 Subject: [PATCH 11/15] remove unused code from ast to ast --- qlasskit/ast2ast/astrewriter.py | 60 +-------------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py index 16bdd823..3c608544 100644 --- a/qlasskit/ast2ast/astrewriter.py +++ b/qlasskit/ast2ast/astrewriter.py @@ -14,25 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import ast -from dataclasses import dataclass -from typing import Optional, Union +from typing import Optional -from .constantfolder import ConstantFolder from .env import Environment -@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 - - class ASTRewriter(ast.NodeTransformer): """Rewrites the ast to a simplified version""" @@ -60,47 +46,3 @@ def visit_AnnAssign(self, node: ast.AnnAssign): 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) - - 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 From 6f27bde440e5f0409e16d71ba5374aeca06600e6 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 17:49:21 +0100 Subject: [PATCH 12/15] migrate last ast2ast visitors --- qlasskit/ast2ast/__init__.py | 1 - qlasskit/ast2ast/ast2ast.py | 4 -- qlasskit/ast2ast/astrewriter.py | 48 -------------------- qlasskit/ast2ast/env.py | 72 ------------------------------ qlasskit/ast2logic/t_expression.py | 4 ++ qlasskit/ast2logic/t_statement.py | 19 ++++++-- 6 files changed, 20 insertions(+), 128 deletions(-) delete mode 100644 qlasskit/ast2ast/astrewriter.py delete mode 100644 qlasskit/ast2ast/env.py diff --git a/qlasskit/ast2ast/__init__.py b/qlasskit/ast2ast/__init__.py index 1504b3c5..b1950094 100644 --- a/qlasskit/ast2ast/__init__.py +++ b/qlasskit/ast2ast/__init__.py @@ -15,5 +15,4 @@ # 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 index a91a9df2..8abbe359 100644 --- a/qlasskit/ast2ast/ast2ast.py +++ b/qlasskit/ast2ast/ast2ast.py @@ -15,7 +15,6 @@ # 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 @@ -33,9 +32,6 @@ def ast2ast(a_tree: ast.AST) -> ast.AST: # 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) diff --git a/qlasskit/ast2ast/astrewriter.py b/qlasskit/ast2ast/astrewriter.py deleted file mode 100644 index 3c608544..00000000 --- a/qlasskit/ast2ast/astrewriter.py +++ /dev/null @@ -1,48 +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 Optional - -from .env import Environment - - -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 - - 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_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 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/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 9098fb34..295b271a 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -61,6 +61,10 @@ def __init__(self, env: Env): # 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()) diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index 5698008d..b90f0b2c 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -67,6 +67,10 @@ 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)) @@ -216,9 +220,9 @@ 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): + if isinstance(arg, (ast.Tuple, ast.List)): return arg.elts - if isinstance(arg, ast.Constant) and isinstance(arg.value, ast.Tuple): + 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) @@ -299,7 +303,9 @@ def __unroll_for(stmt: ast.For, env: Env) -> List[ast.stmt]: rolls: List[ast.stmt] = [] for i in it_vals: _val = ( - i if isinstance(i, (ast.Constant, ast.Subscript)) else ast.Constant(value=i) + 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: @@ -379,6 +385,13 @@ def translate_statement( # noqa: C901 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) From 92c9d33c197e852cd9e8fd08b59a25bebdd8ffce Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Wed, 11 Nov 2026 20:18:42 +0100 Subject: [PATCH 13/15] move preprocessing to ast2logic, remove old ast2ast package --- pyproject.toml | 1 - qlasskit/__init__.py | 3 +- qlasskit/ast2ast/__init__.py | 18 --- qlasskit/ast2ast/ast2ast.py | 39 ----- qlasskit/ast2ast/replacemultitargetassign.py | 55 ------- qlasskit/ast2ast/replacetypeann.py | 105 ------------- qlasskit/ast2logic/__init__.py | 1 + .../preprocess.py} | 146 ++++++++++++++++++ qlasskit/qlassfun.py | 5 +- test/ast2logic/test_translate_arg.py | 24 +-- 10 files changed, 162 insertions(+), 235 deletions(-) delete mode 100644 qlasskit/ast2ast/__init__.py delete mode 100644 qlasskit/ast2ast/ast2ast.py delete mode 100644 qlasskit/ast2ast/replacemultitargetassign.py delete mode 100644 qlasskit/ast2ast/replacetypeann.py rename qlasskit/{ast2ast/constantfolder.py => ast2logic/preprocess.py} (52%) 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 b1950094..00000000 --- a/qlasskit/ast2ast/__init__.py +++ /dev/null @@ -1,18 +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 .constantfolder import ConstantFolder # noqa: F401 diff --git a/qlasskit/ast2ast/ast2ast.py b/qlasskit/ast2ast/ast2ast.py deleted file mode 100644 index 8abbe359..00000000 --- a/qlasskit/ast2ast/ast2ast.py +++ /dev/null @@ -1,39 +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 .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) - - # Fold constants again - a_tree = ConstantFolder().visit(a_tree) - - # print(ast.dump(a_tree)) - return a_tree 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/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..e9ca1c54 100644 --- a/qlasskit/ast2ast/constantfolder.py +++ b/qlasskit/ast2logic/preprocess.py @@ -13,9 +13,13 @@ # # 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): @@ -134,3 +138,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/qlassfun.py b/qlasskit/qlassfun.py index 7f04cb38..6c9fda0a 100644 --- a/qlasskit/qlassfun.py +++ b/qlasskit/qlassfun.py @@ -31,8 +31,7 @@ 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 +321,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/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) From 52a43f570739153ad53bf163f8a0688e0f449d01 Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Thu, 12 Nov 2026 08:04:55 +0100 Subject: [PATCH 14/15] linting and typefix --- qlasskit/ast2logic/preprocess.py | 2 ++ qlasskit/ast2logic/t_expression.py | 15 ++++++++------- qlasskit/ast2logic/t_statement.py | 2 +- qlasskit/qlassfun.py | 10 +++++++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/qlasskit/ast2logic/preprocess.py b/qlasskit/ast2logic/preprocess.py index e9ca1c54..b42c78c7 100644 --- a/qlasskit/ast2logic/preprocess.py +++ b/qlasskit/ast2logic/preprocess.py @@ -23,6 +23,8 @@ class ConstantFolder(ast.NodeTransformer): + """Folds expression with constants to the result""" + def __init__(self): self.builtin_funcs = { "abs": abs, diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 295b271a..19348181 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -482,7 +482,7 @@ def __unroll_arg(self, arg) -> List: ): t = self.env[arg.value.id].ttype if not hasattr(t, "BIT_SIZE") and get_args(t): - inner = get_args(t)[arg.slice.value] + inner = get_args(t)[arg.slice.value] # type: ignore n = ( len(get_args(inner)) if get_args(inner) @@ -500,9 +500,7 @@ def __unroll_arg(self, arg) -> List: 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)]) - ) + 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: @@ -522,8 +520,11 @@ def rec(lst): def __call_anyall(self, node: ast.Call): if len(node.args) != 1: - raise Exception(f"{node.func.id}() takes exactly 1 argument ({len(node.args)} given)") - op = Or if node.func.id == "any" else And + 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): @@ -541,7 +542,7 @@ def __call_minmax(self, node: ast.Call): 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" + cmp = "gt" if node.func.id == "max" else "lte" # type: ignore def rec(lst): if len(lst) == 1: diff --git a/qlasskit/ast2logic/t_statement.py b/qlasskit/ast2logic/t_statement.py index b90f0b2c..80dc7651 100644 --- a/qlasskit/ast2logic/t_statement.py +++ b/qlasskit/ast2logic/t_statement.py @@ -296,7 +296,7 @@ def __unroll_for(stmt: ast.For, env: Env) -> List[ast.stmt]: if b is True or str(b) == "True": val |= 1 << i args.append(val) - it_vals: List = list(range(*args)) + it_vals: List = list(range(*args)) # type: ignore else: it_vals = flatten(__unroll_arg(it, env)) diff --git a/qlasskit/qlassfun.py b/qlasskit/qlassfun.py index 6c9fda0a..a6c75684 100644 --- a/qlasskit/qlassfun.py +++ b/qlasskit/qlassfun.py @@ -31,7 +31,15 @@ from sympy import Symbol -from .ast2logic import Arg, Args, BoolExpList, LogicFun, flatten, preprocess_ast, 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 From 0ebc9bebd6b0c14347a40b3282c313141b888d1a Mon Sep 17 00:00:00 2001 From: Davide Gessa Date: Thu, 12 Nov 2026 08:49:34 +0100 Subject: [PATCH 15/15] us dispatcher in ast2logic and compiler --- qlasskit/ast2logic/t_expression.py | 40 +++++++++---------- qlasskit/compiler/internalcompiler.py | 55 +++++++++++---------------- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 19348181..d9c15cc7 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -59,6 +59,17 @@ def __init__(self, env: Env): "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 @@ -392,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) @@ -405,11 +415,7 @@ 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.Pow) and hasattr(tleft[0], "mul"): + 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 @@ -426,19 +432,6 @@ def visit_BinOp(self, expr: ast.BinOp): # noqa: C901 return const_to_qtype(1) else: raise exceptions.ExpressionNotHandledException(expr.right) - - 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) elif ( isinstance(expr.op, ast.LShift) and hasattr(tleft[0], "shift_left") @@ -452,7 +445,12 @@ 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, 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()