diff --git a/makefile b/makefile index 94530c3..b36d0ed 100644 --- a/makefile +++ b/makefile @@ -4,7 +4,9 @@ all: build: python -m pip install -vv -e . test: - coverage run -m pytest --pyargs numba_rvsdg + # Activate using the sys.monitoring implementation of coverage. + # Needs at least coverage veraion 7.4.0 to work. + COVERAGE_CORE=sysmon coverage run -m pytest --pyargs numba_rvsdg coverage report lint: pre-commit run --verbose --all-files diff --git a/numba_rvsdg/__init__.py b/numba_rvsdg/__init__.py index 3c59890..d29d637 100644 --- a/numba_rvsdg/__init__.py +++ b/numba_rvsdg/__init__.py @@ -1 +1,4 @@ -from numba_rvsdg.core.datastructures.ast_transforms import AST2SCFG # noqa +from numba_rvsdg.core.datastructures.ast_transforms import ( # noqa + AST2SCFG, + SCFG2AST, +) diff --git a/numba_rvsdg/core/datastructures/ast_transforms.py b/numba_rvsdg/core/datastructures/ast_transforms.py index 30a93ad..34edea1 100644 --- a/numba_rvsdg/core/datastructures/ast_transforms.py +++ b/numba_rvsdg/core/datastructures/ast_transforms.py @@ -1,11 +1,43 @@ import ast import inspect +import itertools from typing import Callable, Any, MutableMapping import textwrap from dataclasses import dataclass +from collections import defaultdict from numba_rvsdg.core.datastructures.scfg import SCFG -from numba_rvsdg.core.datastructures.basic_block import PythonASTBlock +from numba_rvsdg.core.datastructures.basic_block import ( + PythonASTBlock, + RegionBlock, + SyntheticHead, + SyntheticTail, + SyntheticFill, + SyntheticReturn, + SyntheticAssignment, + SyntheticExitingLatch, + SyntheticExitBranch, +) + + +def unparse_code( + code: str | list[ast.FunctionDef] | Callable[..., Any] +) -> list[type[ast.AST]]: + # Convert source code into AST. + if isinstance(code, str): + tree = ast.parse(code).body + elif callable(code): + tree = ast.parse(textwrap.dedent(inspect.getsource(code))).body + elif ( + isinstance(code, list) + and len(code) > 0 + and all([isinstance(i, ast.AST) for i in code]) + ): + tree = code # type: ignore + else: + msg = "Type: '{type(self.code}}' is not implemented." + raise NotImplementedError(msg) + return tree # type: ignore class WritableASTBlock: @@ -217,7 +249,7 @@ def __init__( ) -> None: self.prune = prune self.code = code - self.tree = AST2SCFGTransformer.unparse_code(code) + self.tree = unparse_code(code) self.block_index: int = 1 # 0 is reserved for genesis block self.blocks = ASTCFG() # Initialize first (genesis) block, assume it's named zero. @@ -225,26 +257,6 @@ def __init__( self.add_block(0) self.loop_stack: list[LoopIndices] = [] - @staticmethod - def unparse_code( - code: str | list[ast.FunctionDef] | Callable[..., Any] - ) -> list[type[ast.AST]]: - # Convert source code into AST. - if isinstance(code, str): - tree = ast.parse(code).body - elif callable(code): - tree = ast.parse(textwrap.dedent(inspect.getsource(code))).body - elif ( - isinstance(code, list) - and len(code) > 0 - and all([isinstance(i, ast.AST) for i in code]) - ): - tree = code # type: ignore - else: - msg = "Type: '{type(self.code}}' is not implemented." - raise NotImplementedError(msg) - return tree # type: ignore - def transform_to_ASTCFG(self) -> ASTCFG: """Generate ASTCFG from Python function.""" self.transform() @@ -329,7 +341,7 @@ def handle_function_def(self, node: ast.FunctionDef) -> None: # end up being an unreachable block if all other paths through the # program already call return. if not isinstance(node.body[-1], ast.Return): - node.body.append(ast.Return(None)) + node.body.append(ast.Return()) self.codegen(node.body) def handle_if(self, node: ast.If) -> None: @@ -574,8 +586,8 @@ def function(a: int) -> None # the CFG. target = ast.unparse(node.target) iter_setup = ast.unparse(node.iter) - iter_assign = f"__iterator_{head_index}__" - last_target_value = f"__iter_last_{head_index}__" + iter_assign = f"__scfg_iterator_{head_index}__" + last_target_value = f"__scfg_iter_last_{head_index}__" # Emit iterator setup to pre-header. preheader_code = textwrap.dedent( @@ -593,14 +605,14 @@ def function(a: int) -> None # Emit header instructions. This first makes a backup of the iteration # target and then checks if the iterator is exhausted and if the loop - # should continue. The '__sentinel__' is an singleton style marker, so - # it need not be versioned. + # should continue. The '__scfg__sentinel__' is an singleton style + # marker, so it need not be versioned. header_code = textwrap.dedent( f""" {last_target_value} = {target} - {target} = next({iter_assign}, "__sentinel__") - {target} != "__sentinel__" + {target} = next({iter_assign}, "__scfg_sentinel__") + {target} != "__scfg_sentinel__" """ ) self.codegen(ast.parse(header_code).body) @@ -655,11 +667,213 @@ def render(self) -> None: self.blocks.to_SCFG().render() +class SCFG2ASTTransformer: + + def transform( + self, original: ast.FunctionDef, scfg: SCFG + ) -> ast.FunctionDef: + body: list[ast.AST] = [] + self.region_stack = [scfg.region] + self.scfg = scfg + for name, block in scfg.concealed_region_view.items(): + if type(block) is RegionBlock and block.kind == "branch": + continue + body.extend(self.codegen(block)) + fdef = ast.FunctionDef( + name="transformed_function", + args=original.args, + body=body, + lineno=0, + decorator_list=original.decorator_list, + returns=original.returns, + ) + return fdef + + def lookup(self, item: Any) -> Any: + subregion_scfg = self.region_stack[-1].subregion + parent_region_block = self.region_stack[-1].parent_region + if item in subregion_scfg: # type: ignore + return subregion_scfg[item] # type: ignore + else: + return self.rlookup(parent_region_block, item) # type: ignore + + def rlookup(self, region_block: RegionBlock, item: Any) -> Any: + if item in region_block.subregion: # type: ignore + return region_block.subregion[item] # type: ignore + elif region_block.parent_region is not None: + return self.rlookup(region_block.parent_region, item) + else: + raise KeyError(f"Item {item} not found in subregion or parent") + + def codegen(self, block: Any) -> list[ast.AST]: + if type(block) is PythonASTBlock: + if len(block.jump_targets) == 2: + if type(block.tree[-1]) in (ast.Name, ast.Compare): + test = block.tree[-1] + else: + test = block.tree[-1].value # type: ignore + body = self.codegen(self.lookup(block.jump_targets[0])) + orelse = self.codegen(self.lookup(block.jump_targets[1])) + if_node = ast.If(test, body, orelse) + return block.tree[:-1] + [if_node] + elif block.fallthrough and type(block.tree[-1]) is ast.Return: + # The value of the ast.Return could be either None or an + # ast.AST type. In the case of None, this refers to a plain + # 'return', which is implicitly 'return None'. So, if it is + # None, we assign the __scfg_return_value__ an + # ast.Constant(None) and whatever the ast.AST node is + # otherwise. + val = block.tree[-1].value + return block.tree[:-1] + [ + ast.Assign( + [ast.Name("__scfg_return_value__")], + (ast.Constant(None) if val is None else val), + lineno=0, + ) + ] + elif block.fallthrough or block.is_exiting: + return block.tree + else: + raise NotImplementedError + elif type(block) is RegionBlock: + # We maintain a stack of the current region, in order to allow for + # random node lookup by name. + self.region_stack.append(block) + + # This is a custom view that uses the concealed_region_view and + # additionally filters all branch regions. Essentially, branch + # regions will be visited by calling codegen recursively from + # blocks with multiple jump targets and all other regions must be + # visited linearly. + def codegen_view() -> list[Any]: + return list( + itertools.chain.from_iterable( + self.codegen(b) + for b in block.subregion.concealed_region_view.values() # type: ignore # noqa + if not (type(b) is RegionBlock and b.kind == "branch") + ) + ) + + if block.kind in ("head", "tail", "branch"): + rval = codegen_view() + elif block.kind == "loop": + # A loop region gives rise to a Python while __scfg_loop_cont__ + # loop. We recursively visit the body. The exiting latch will + # update __scfg_loop_continue__. + rval = [ + ast.Assign( + [ast.Name("__scfg_loop_cont__")], + ast.Constant(True), + lineno=0, + ), + ast.While( + test=ast.Name("__scfg_loop_cont__"), + body=codegen_view(), + orelse=[], + ), + ] + else: + raise NotImplementedError + self.region_stack.pop() + return rval + elif type(block) is SyntheticAssignment: + # Synthetic assignments just create Python assignments, one for + # each variable.. + return [ + ast.Assign([ast.Name(t)], ast.Constant(v), lineno=0) + for t, v in block.variable_assignment.items() + ] + elif type(block) is SyntheticTail: + # Synthetic tails do nothing. + return [] + elif type(block) is SyntheticFill: + # Synthetic fills must have a pass statement to main syntactical + # correctness of the final program. + return [ast.Pass()] + elif type(block) is SyntheticReturn: + # Synthetic return blocks must re-assigne the return value to a + # special reserved variable. + return [ast.Return(ast.Name("__scfg_return_value__"))] + elif type(block) is SyntheticExitingLatch: + # The synthetic exiting latch simply assigns the negated value of + # the exit variable to '__scfg_loop_cont__'. + assert len(block.jump_targets) == 1 + assert len(block.backedges) == 1 + return [ + ast.Assign( + [ast.Name("__scfg_loop_cont__")], + ast.UnaryOp(ast.Not(), ast.Name(block.variable)), + lineno=0, + ) + ] + elif type(block) in (SyntheticExitBranch, SyntheticHead): + # Both the Synthetic exit branch and the synthetic head contain a + # branching statement with potentially multiple outgoing branches. + # This means we must recursively generate an if-cascade in Python, + # such that all jump targets may be visisted. Looking at the + # resulting AST, it does appear as though the compilation of the + # AST to source code will use `elif` statements. + + # Create a reverse lookup from the branch_value_table + # branch_name --> list of variables that lead there + reverse = defaultdict(list) + for ( + variable_value, + jump_target, + ) in block.branch_value_table.items(): + reverse[jump_target].append(variable_value) + # recursive generation of if-cascade + + def if_cascade(jump_targets: list[str]) -> list[ast.AST]: + if len(jump_targets) == 1: + # base case, final else + return self.codegen(self.lookup(jump_targets.pop())) + else: + # otherwise generate if statement for current jump_target + current = jump_targets.pop() + # compare to all variable values that point to this + # jump_target + if_test = ast.Compare( + left=ast.Name(block.variable), + ops=[ast.In()], + comparators=[ + ast.Tuple( + elts=[ + ast.Constant(i) for i in reverse[current] + ], + ctx=ast.Load(), + ) + ], + ) + # Create the the if-statement itself, using the test. Do + # code-gen for the block that the is being pointed to and + # recurse for the rest of the jump_targets. + if_node = ast.If( + test=if_test, + body=self.codegen(self.lookup(current)), + orelse=if_cascade(jump_targets), + ) + return [if_node] + + # Send in a copy of the jump_targets as this list will be mutated. + return if_cascade(list(block.jump_targets[::-1])) + else: + raise NotImplementedError + + raise NotImplementedError("unreachable") + + def AST2SCFG(code: str | list[ast.FunctionDef] | Callable[..., Any]) -> SCFG: """Transform Python function into an SCFG.""" return AST2SCFGTransformer(code).transform_to_SCFG() -def SCFG2AST(scfg: SCFG) -> ast.FunctionDef: # type: ignore - """Transform SCFG with PythonASTBlocks into an AST FunctionDef.""" - # TODO +def SCFG2AST( + code: str | list[ast.FunctionDef] | Callable[..., Any], scfg: SCFG +) -> ast.FunctionDef: + """Transform SCFG with PythonASTBlocks into an AST FunctionDef defined in + code.""" + original_ast = unparse_code(code)[0] + return SCFG2ASTTransformer().transform( + original=original_ast, scfg=scfg # type: ignore + ) diff --git a/numba_rvsdg/core/datastructures/scfg.py b/numba_rvsdg/core/datastructures/scfg.py index f598b36..016622e 100644 --- a/numba_rvsdg/core/datastructures/scfg.py +++ b/numba_rvsdg/core/datastructures/scfg.py @@ -138,11 +138,11 @@ def new_var_name(self, kind: str) -> str: """ if kind in self.kinds.keys(): idx = self.kinds[kind] - name = str(kind) + "_var_" + str(idx) + name = "__scfg_" + str(kind) + "_var_" + str(idx) + "__" self.kinds[kind] = idx + 1 else: idx = 0 - name = str(kind) + "_var_" + str(idx) + name = "__scfg_" + str(kind) + "_var_" + str(idx) + "__" self.kinds[kind] = idx + 1 return name diff --git a/numba_rvsdg/tests/test_ast_transforms.py b/numba_rvsdg/tests/test_ast_transforms.py index 2377eb0..bfc7dc0 100644 --- a/numba_rvsdg/tests/test_ast_transforms.py +++ b/numba_rvsdg/tests/test_ast_transforms.py @@ -3,8 +3,26 @@ import textwrap from typing import Callable, Any from unittest import main, TestCase +from sys import monitoring as sm -from numba_rvsdg.core.datastructures.ast_transforms import AST2SCFGTransformer +from numba_rvsdg.core.datastructures.ast_transforms import ( + unparse_code, + AST2SCFGTransformer, + SCFG2ASTTransformer, + AST2SCFG, + SCFG2AST, +) + +sm.use_tool_id(sm.PROFILER_ID, "custom_tracer") + + +class LineTraceCallback: + + def __init__(self): + self.lines = set() + + def __call__(self, code, line): + self.lines.add(line) class TestAST2SCFGTransformer(TestCase): @@ -15,14 +33,97 @@ def compare( expected: dict[str, dict[str, Any]], unreachable: set[int] = set(), empty: set[int] = set(), + arguments: list[any] = [], ): - transformer = AST2SCFGTransformer(function) - astcfg = transformer.transform_to_ASTCFG() + # Execute function with first argument, if given. Ensure function is + # sane and make sure it's picked up by coverage. + try: + if arguments: + for a in arguments: + _ = function(*a) + else: + _ = function() + except Exception: + pass + # First, test against the expected CFG... + ast2scfg_transformer = AST2SCFGTransformer(function) + astcfg = ast2scfg_transformer.transform_to_ASTCFG() self.assertEqual(expected, astcfg.to_dict()) self.assertEqual(unreachable, {i.name for i in astcfg.unreachable}) self.assertEqual(empty, {i.name for i in astcfg.empty}) + # Then restructure, synthesize python and run original and transformed + # on the same arguments and assert they are the same. + scfg = astcfg.to_SCFG() + scfg.restructure() + scfg2ast = SCFG2ASTTransformer() + original_ast = unparse_code(function)[0] + transformed_ast = scfg2ast.transform(original=original_ast, scfg=scfg) + + # use exec to obtin the function and the transformed_function + original_exec_locals = {} + exec(ast.unparse(original_ast), {}, original_exec_locals) + temporary_function = original_exec_locals["function"] + temporary_exec_locals = {} + exec(ast.unparse(transformed_ast), {}, temporary_exec_locals) + temporary_transformed_function = temporary_exec_locals[ + "transformed_function" + ] + + # Setup the profiler for both funstions and initialize the callbacks + sm.set_local_events( + sm.PROFILER_ID, temporary_function.__code__, sm.events.LINE + ) + sm.set_local_events( + sm.PROFILER_ID, + temporary_transformed_function.__code__, + sm.events.LINE, + ) + original_callback = LineTraceCallback() + transformed_callback = LineTraceCallback() + + # Register the callbacks one at a time and collect results. + sm.register_callback(sm.PROFILER_ID, sm.events.LINE, original_callback) + if arguments: + original_results = [temporary_function(*a) for a in arguments] + else: + original_results = [temporary_function()] + + # Only one callback can be registered at a time. + sm.register_callback( + sm.PROFILER_ID, sm.events.LINE, transformed_callback + ) + if arguments: + transformed_results = [ + temporary_transformed_function(*a) for a in arguments + ] + else: + transformed_results = [temporary_transformed_function()] + + assert len(original_callback.lines) > 0 + assert len(transformed_callback.lines) > 0 + + # Check call results + assert original_results == transformed_results + + # Check line trace of original + original_source = ast.unparse(original_ast).splitlines() + assert [ + i + 1 + for i, l in enumerate(original_source) + if not l.startswith("def") and "else:" not in l + ] == sorted(original_callback.lines) + + # Check line trace of transformed + transformed_source = ast.unparse(transformed_ast).splitlines() + assert [ + i + 1 + for i, l in enumerate(transformed_source) + if not l.startswith("def") and "else:" not in l + ] == sorted(transformed_callback.lines) + def setUp(self): + # Enable pytest verbose output. self.maxDiff = None def test_solo_return(self): @@ -116,13 +217,13 @@ def function() -> int: def test_if_return(self): def function(x: int) -> int: - if x < 10: + if x < 1: return 1 return 2 expected = { "0": { - "instructions": ["x < 10"], + "instructions": ["x < 1"], "jump_targets": ["1", "3"], "name": "0", }, @@ -137,18 +238,18 @@ def function(x: int) -> int: "name": "3", }, } - self.compare(function, expected, empty={"2"}) + self.compare(function, expected, empty={"2"}, arguments=[(0,), (1,)]) def test_if_else_return(self): def function(x: int) -> int: - if x < 10: + if x < 1: return 1 else: return 2 expected = { "0": { - "instructions": ["x < 10"], + "instructions": ["x < 1"], "jump_targets": ["1", "2"], "name": "0", }, @@ -163,174 +264,199 @@ def function(x: int) -> int: "name": "2", }, } - self.compare(function, expected, unreachable={"3"}) + self.compare( + function, expected, unreachable={"3"}, arguments=[(0,), (1,)] + ) def test_if_else_assign(self): def function(x: int) -> int: - if x < 10: - z = 1 + if x < 1: + y = 1 else: - z = 2 - return z + y = 2 + return y expected = { "0": { - "instructions": ["x < 10"], + "instructions": ["x < 1"], "jump_targets": ["1", "2"], "name": "0", }, "1": { - "instructions": ["z = 1"], + "instructions": ["y = 1"], "jump_targets": ["3"], "name": "1", }, "2": { - "instructions": ["z = 2"], + "instructions": ["y = 2"], "jump_targets": ["3"], "name": "2", }, "3": { - "instructions": ["return z"], + "instructions": ["return y"], "jump_targets": [], "name": "3", }, } - self.compare(function, expected) + self.compare(function, expected, arguments=[(0,), (1,)]) def test_nested_if(self): def function(x: int, y: int) -> int: - if x < 10: - if y < 5: - y = 1 + if x < 1: + if y < 1: + z = 1 else: - y = 2 + z = 2 else: - if y < 15: - y = 3 + if y < 2: + z = 3 else: - y = 4 - return y + z = 4 + return z expected = { "0": { - "instructions": ["x < 10"], + "instructions": ["x < 1"], "jump_targets": ["1", "2"], "name": "0", }, "1": { - "instructions": ["y < 5"], + "instructions": ["y < 1"], "jump_targets": ["4", "5"], "name": "1", }, "2": { - "instructions": ["y < 15"], + "instructions": ["y < 2"], "jump_targets": ["7", "8"], "name": "2", }, "3": { - "instructions": ["return y"], + "instructions": ["return z"], "jump_targets": [], "name": "3", }, "4": { - "instructions": ["y = 1"], + "instructions": ["z = 1"], "jump_targets": ["3"], "name": "4", }, "5": { - "instructions": ["y = 2"], + "instructions": ["z = 2"], "jump_targets": ["3"], "name": "5", }, "7": { - "instructions": ["y = 3"], + "instructions": ["z = 3"], "jump_targets": ["3"], "name": "7", }, "8": { - "instructions": ["y = 4"], + "instructions": ["z = 4"], "jump_targets": ["3"], "name": "8", }, } - self.compare(function, expected, empty={"6", "9"}) + self.compare( + function, + expected, + empty={"6", "9"}, + arguments=[(0, 0), (0, 1), (1, 1), (1, 2)], + ) def test_nested_if_with_empty_else_and_return(self): def function(x: int, y: int) -> None: - y << 2 - if x < 10: - y -= 1 - if y < 5: - y = 1 + z = 0 + if x < 1: + y << 2 + if y < 1: + z = 1 else: - if y < 15: - y = 2 + if y < 2: + z = 2 else: - return + return y, 4 y += 1 - return y + return y, z expected = { "0": { - "instructions": ["y << 2", "x < 10"], + "instructions": ["z = 0", "x < 1"], "jump_targets": ["1", "2"], "name": "0", }, "1": { - "instructions": ["y -= 1", "y < 5"], + "instructions": ["y << 2", "y < 1"], "jump_targets": ["4", "3"], "name": "1", }, "2": { - "instructions": ["y < 15"], + "instructions": ["y < 2"], "jump_targets": ["7", "8"], "name": "2", }, "3": { - "instructions": ["return y"], + "instructions": ["return (y, z)"], "jump_targets": [], "name": "3", }, "4": { - "instructions": ["y = 1"], + "instructions": ["z = 1"], "jump_targets": ["3"], "name": "4", }, "7": { - "instructions": ["y = 2"], + "instructions": ["z = 2"], "jump_targets": ["9"], "name": "7", }, - "8": {"instructions": ["return"], "jump_targets": [], "name": "8"}, + "8": { + "instructions": ["return (y, 4)"], + "jump_targets": [], + "name": "8", + }, "9": { "instructions": ["y += 1"], "jump_targets": ["3"], "name": "9", }, } - self.compare(function, expected, empty={"5", "6"}) + self.compare( + function, + expected, + empty={"5", "6"}, + arguments=[ + (0, 0), + (0, 1), + (1, 1), + (1, 2), + ], + ) def test_elif(self): - def function(x: int, a: int, b: int) -> int: - if x < 10: - return - elif x < 15: - y = b - a - elif x < 20: - y = a**2 + def function(x: int) -> int: + if x < 1: + return 10 + elif x < 2: + y = 20 + elif x < 3: + y = 30 else: - y = a - b + y = 40 return y expected = { "0": { - "instructions": ["x < 10"], + "instructions": ["x < 1"], "jump_targets": ["1", "2"], "name": "0", }, - "1": {"instructions": ["return"], "jump_targets": [], "name": "1"}, + "1": { + "instructions": ["return 10"], + "jump_targets": [], + "name": "1", + }, "2": { - "instructions": ["x < 15"], + "instructions": ["x < 2"], "jump_targets": ["4", "5"], "name": "2", }, @@ -340,27 +466,37 @@ def function(x: int, a: int, b: int) -> int: "name": "3", }, "4": { - "instructions": ["y = b - a"], + "instructions": ["y = 20"], "jump_targets": ["3"], "name": "4", }, "5": { - "instructions": ["x < 20"], + "instructions": ["x < 3"], "jump_targets": ["7", "8"], "name": "5", }, "7": { - "instructions": ["y = a ** 2"], + "instructions": ["y = 30"], "jump_targets": ["3"], "name": "7", }, "8": { - "instructions": ["y = a - b"], + "instructions": ["y = 40"], "jump_targets": ["3"], "name": "8", }, } - self.compare(function, expected, empty={"9", "6"}) + self.compare( + function, + expected, + empty={"9", "6"}, + arguments=[ + (0,), + (1,), + (2,), + (3,), + ], + ) def test_simple_while(self): def function() -> int: @@ -483,9 +619,9 @@ def function() -> int: self.compare(function, expected, empty={"4", "7"}) def test_while_in_if(self): - def function(a: bool) -> int: + def function(y: int) -> int: x = 0 - if a is True: + if y == 0: while x < 10: x += 2 else: @@ -495,7 +631,7 @@ def function(a: bool) -> int: expected = { "0": { - "instructions": ["x = 0", "a is True"], + "instructions": ["x = 0", "y == 0"], "jump_targets": ["4", "8"], "name": "0", }, @@ -526,55 +662,63 @@ def function(a: bool) -> int: }, } self.compare( - function, expected, empty={"1", "2", "6", "7", "10", "11"} + function, + expected, + empty={"1", "2", "6", "7", "10", "11"}, + arguments=[(0,), (1,)], ) def test_while_break_continue(self): - def function() -> int: - x = 0 - while x < 10: - x += 1 - if x % 2 == 0: + def function(x: int) -> int: + y = 0 + while y < 10: + y += 1 + if x == 0: continue - elif x == 9: + elif x == 1: break else: - x += 1 - return x + y += 10 + return y expected = { "0": { - "instructions": ["x = 0"], + "instructions": ["y = 0"], "jump_targets": ["1"], "name": "0", }, "1": { - "instructions": ["x < 10"], + "instructions": ["y < 10"], "jump_targets": ["2", "3"], "name": "1", }, "2": { - "instructions": ["x += 1", "x % 2 == 0"], + "instructions": ["y += 1", "x == 0"], "jump_targets": ["1", "6"], "name": "2", }, "3": { - "instructions": ["return x"], + "instructions": ["return y"], "jump_targets": [], "name": "3", }, "6": { - "instructions": ["x == 9"], + "instructions": ["x == 1"], "jump_targets": ["3", "9"], "name": "6", }, "9": { - "instructions": ["x += 1"], + "instructions": ["y += 10"], "jump_targets": ["1"], "name": "9", }, } - self.compare(function, expected, empty={"4", "5", "7", "8", "10"}) + self.compare( + function, + expected, + empty={"4", "5", "7", "8", "10"}, + arguments=[(0,), (1,), (2,)], + ) def test_while_else(self): def function() -> int: @@ -616,16 +760,16 @@ def function() -> int: def test_simple_for(self): def function() -> int: - c = 0 + x = 0 for i in range(10): - c += i - return c + x += i + return x, i expected = { "0": { "instructions": [ - "c = 0", - "__iterator_1__ = iter(range(10))", + "x = 0", + "__scfg_iterator_1__ = iter(range(10))", "i = None", ], "jump_targets": ["1"], @@ -633,25 +777,25 @@ def function() -> int: }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "2": { - "instructions": ["c += i"], + "instructions": ["x += i"], "jump_targets": ["1"], "name": "2", }, "3": { - "instructions": ["i = __iter_last_1__"], + "instructions": ["i = __scfg_iter_last_1__"], "jump_targets": ["4"], "name": "3", }, "4": { - "instructions": ["return c"], + "instructions": ["return (x, i)"], "jump_targets": [], "name": "4", }, @@ -660,18 +804,18 @@ def function() -> int: def test_nested_for(self): def function() -> int: - c = 0 + x = 0 for i in range(3): - c += i + x += i for j in range(3): - c += j - return c + x += j + return x, i, j expected = { "0": { "instructions": [ - "c = 0", - "__iterator_1__ = iter(range(3))", + "x = 0", + "__scfg_iterator_1__ = iter(range(3))", "i = None", ], "jump_targets": ["1"], @@ -679,48 +823,48 @@ def function() -> int: }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "2": { "instructions": [ - "c += i", - "__iterator_5__ = iter(range(3))", + "x += i", + "__scfg_iterator_5__ = iter(range(3))", "j = None", ], "jump_targets": ["5"], "name": "2", }, "3": { - "instructions": ["i = __iter_last_1__"], + "instructions": ["i = __scfg_iter_last_1__"], "jump_targets": ["4"], "name": "3", }, "4": { - "instructions": ["return c"], + "instructions": ["return (x, i, j)"], "jump_targets": [], "name": "4", }, "5": { "instructions": [ - "__iter_last_5__ = j", - "j = next(__iterator_5__, '__sentinel__')", - "j != '__sentinel__'", + "__scfg_iter_last_5__ = j", + "j = next(__scfg_iterator_5__, '__scfg_sentinel__')", + "j != '__scfg_sentinel__'", ], "jump_targets": ["6", "7"], "name": "5", }, "6": { - "instructions": ["c += j"], + "instructions": ["x += j"], "jump_targets": ["5"], "name": "6", }, "7": { - "instructions": ["j = __iter_last_5__"], + "instructions": ["j = __scfg_iter_last_5__"], "jump_targets": ["1"], "name": "7", }, @@ -728,12 +872,12 @@ def function() -> int: self.compare(function, expected, empty={"8"}) def test_for_with_return_break_and_continue(self): - def function(a: int, b: int) -> int: + def function(x: int, y: int) -> int: for i in range(2): - if i == a: + if i == x: i = 3 return i - elif i == b: + elif i == y: i = 4 break else: @@ -743,7 +887,7 @@ def function(a: int, b: int) -> int: expected = { "0": { "instructions": [ - "__iterator_1__ = iter(range(2))", + "__scfg_iterator_1__ = iter(range(2))", "i = None", ], "jump_targets": ["1"], @@ -751,20 +895,20 @@ def function(a: int, b: int) -> int: }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "2": { - "instructions": ["i == a"], + "instructions": ["i == x"], "jump_targets": ["5", "6"], "name": "2", }, "3": { - "instructions": ["i = __iter_last_1__"], + "instructions": ["i = __scfg_iter_last_1__"], "jump_targets": ["4"], "name": "3", }, @@ -779,7 +923,7 @@ def function(a: int, b: int) -> int: "name": "5", }, "6": { - "instructions": ["i == b"], + "instructions": ["i == y"], "jump_targets": ["8", "1"], "name": "6", }, @@ -789,25 +933,31 @@ def function(a: int, b: int) -> int: "name": "8", }, } - self.compare(function, expected, unreachable={"7", "10"}, empty={"9"}) + self.compare( + function, + expected, + unreachable={"7", "10"}, + empty={"9"}, + arguments=[(0, 0), (2, 0), (2, 2)], + ) def test_for_with_if_in_else(self): - def function(a: int): - c = 0 + def function(y: int): + x = 0 for i in range(10): - c += i + x += i else: - if a: - r = c + if y == 0: + x += 10 else: - r = -1 * c - return r + x += 20 + return (x, y, i) expected = { "0": { "instructions": [ - "c = 0", - "__iterator_1__ = iter(range(10))", + "x = 0", + "__scfg_iterator_1__ = iter(range(10))", "i = None", ], "jump_targets": ["1"], @@ -815,65 +965,65 @@ def function(a: int): }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "2": { - "instructions": ["c += i"], + "instructions": ["x += i"], "jump_targets": ["1"], "name": "2", }, "3": { - "instructions": ["i = __iter_last_1__", "a"], + "instructions": ["i = __scfg_iter_last_1__", "y == 0"], "jump_targets": ["5", "6"], "name": "3", }, "4": { - "instructions": ["return r"], + "instructions": ["return (x, y, i)"], "jump_targets": [], "name": "4", }, "5": { - "instructions": ["r = c"], + "instructions": ["x += 10"], "jump_targets": ["4"], "name": "5", }, "6": { - "instructions": ["r = -1 * c"], + "instructions": ["x += 20"], "jump_targets": ["4"], "name": "6", }, } - self.compare(function, expected, empty={"7"}) + self.compare(function, expected, empty={"7"}, arguments=[(0,), (1,)]) def test_for_with_nested_for_else(self): - def function(a: bool) -> int: - c = 1 + def function(y: int) -> int: + x = 1 for i in range(1): for j in range(1): - if a: - c *= 3 + if y == 0: + x *= 3 break # This break decides, if True skip continue. else: - c *= 5 + x *= 5 continue # Causes break below to be skipped. - c *= 7 + x *= 7 break # Causes the else below to be skipped else: - c *= 9 # Not breaking in inner loop leads here - return c + x *= 9 # Not breaking in inner loop leads here + return x, y, i - self.assertEqual(function(True), 3 * 7) - self.assertEqual(function(False), 5 * 9) + self.assertEqual(function(1)[0], 5 * 9) + self.assertEqual(function(0)[0], 3 * 7) expected = { "0": { "instructions": [ - "c = 1", - "__iterator_1__ = iter(range(1))", + "x = 1", + "__scfg_iterator_1__ = iter(range(1))", "i = None", ], "jump_targets": ["1"], @@ -881,96 +1031,98 @@ def function(a: bool) -> int: }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "2": { "instructions": [ - "__iterator_5__ = iter(range(1))", + "__scfg_iterator_5__ = iter(range(1))", "j = None", ], "jump_targets": ["5"], "name": "2", }, "3": { - "instructions": ["i = __iter_last_1__", "c *= 9"], + "instructions": ["i = __scfg_iter_last_1__", "x *= 9"], "jump_targets": ["4"], "name": "3", }, "4": { - "instructions": ["return c"], + "instructions": ["return (x, y, i)"], "jump_targets": [], "name": "4", }, "5": { "instructions": [ - "__iter_last_5__ = j", - "j = next(__iterator_5__, '__sentinel__')", - "j != '__sentinel__'", + "__scfg_iter_last_5__ = j", + "j = next(__scfg_iterator_5__, '__scfg_sentinel__')", + "j != '__scfg_sentinel__'", ], "jump_targets": ["6", "7"], "name": "5", }, "6": { - "instructions": ["a"], + "instructions": ["y == 0"], "jump_targets": ["9", "5"], "name": "6", }, "7": { - "instructions": ["j = __iter_last_5__", "c *= 5"], + "instructions": ["j = __scfg_iter_last_5__", "x *= 5"], "jump_targets": ["1"], "name": "7", }, "8": { - "instructions": ["c *= 7"], + "instructions": ["x *= 7"], "jump_targets": ["4"], "name": "8", }, "9": { - "instructions": ["c *= 3"], + "instructions": ["x *= 3"], "jump_targets": ["8"], "name": "9", }, } - self.compare(function, expected, empty={"11", "10"}) + self.compare( + function, expected, empty={"11", "10"}, arguments=[(0,), (1,)] + ) def test_for_with_nested_else_return_break_and_continue(self): - def function(a: int, b: int, c: int, d: int, e: int, f: int) -> int: + def function(x: int) -> int: for i in range(2): - if i == a: - i = 3 + if x == 1: + i += 1 return i - elif i == b: - i = 4 + elif x == 2: + i += 2 break - elif i == c: - i = 5 + elif x == 3: + i += 3 continue else: while i < 10: i += 1 - if i == d: - i = 3 + if x == 4: + i += 4 return i - elif i == e: - i = 4 + elif x == 5: + i += 5 break - elif i == f: - i = 5 + elif x == 6: + i += 6 continue else: - i += 1 - return i + i += 7 + return x, i expected = { "0": { "instructions": [ - "__iterator_1__ = iter(range(2))", + "__scfg_iterator_1__ = iter(range(2))", "i = None", ], "jump_targets": ["1"], @@ -978,15 +1130,15 @@ def function(a: int, b: int, c: int, d: int, e: int, f: int) -> int: }, "1": { "instructions": [ - "__iter_last_1__ = i", - "i = next(__iterator_1__, '__sentinel__')", - "i != '__sentinel__'", + "__scfg_iter_last_1__ = i", + "i = next(__scfg_iterator_1__, '__scfg_sentinel__')", + "i != '__scfg_sentinel__'", ], "jump_targets": ["2", "3"], "name": "1", }, "11": { - "instructions": ["i = 5"], + "instructions": ["i += 3"], "jump_targets": ["1"], "name": "11", }, @@ -996,78 +1148,104 @@ def function(a: int, b: int, c: int, d: int, e: int, f: int) -> int: "name": "14", }, "15": { - "instructions": ["i += 1", "i == d"], + "instructions": ["i += 1", "x == 4"], "jump_targets": ["18", "19"], "name": "15", }, "18": { - "instructions": ["i = 3", "return i"], + "instructions": ["i += 4", "return i"], "jump_targets": [], "name": "18", }, "19": { - "instructions": ["i == e"], + "instructions": ["x == 5"], "jump_targets": ["21", "22"], "name": "19", }, "2": { - "instructions": ["i == a"], + "instructions": ["x == 1"], "jump_targets": ["5", "6"], "name": "2", }, "21": { - "instructions": ["i = 4"], + "instructions": ["i += 5"], "jump_targets": ["1"], "name": "21", }, "22": { - "instructions": ["i == f"], + "instructions": ["x == 6"], "jump_targets": ["24", "25"], "name": "22", }, "24": { - "instructions": ["i = 5"], + "instructions": ["i += 6"], "jump_targets": ["14"], "name": "24", }, "25": { - "instructions": ["i += 1"], + "instructions": ["i += 7"], "jump_targets": ["14"], "name": "25", }, "3": { - "instructions": ["i = __iter_last_1__"], + "instructions": ["i = __scfg_iter_last_1__"], "jump_targets": ["4"], "name": "3", }, "4": { - "instructions": ["return i"], + "instructions": ["return (x, i)"], "jump_targets": [], "name": "4", }, "5": { - "instructions": ["i = 3", "return i"], + "instructions": ["i += 1", "return i"], "jump_targets": [], "name": "5", }, "6": { - "instructions": ["i == b"], + "instructions": ["x == 2"], "jump_targets": ["8", "9"], "name": "6", }, "8": { - "instructions": ["i = 4"], + "instructions": ["i += 2"], "jump_targets": ["4"], "name": "8", }, "9": { - "instructions": ["i == c"], + "instructions": ["x == 3"], "jump_targets": ["11", "14"], "name": "9", }, } empty = {"7", "10", "12", "13", "16", "17", "20", "23", "26"} - self.compare(function, expected, empty=empty) + arguments = [(1,), (2,), (3,), (4,), (5,), (6,), (7,)] + self.compare( + function, + expected, + empty=empty, + arguments=arguments, + ) + + +class TestEntryPoints(TestCase): + + def test_rondtrip(self): + + def function() -> int: + x = 0 + for i in range(2): + x += i + return x, i + + scfg = AST2SCFG(function) + scfg.restructure() + ast_ = SCFG2AST(function, scfg) + + exec_locals = {} + exec(ast.unparse(ast_), {}, exec_locals) + transformed = exec_locals["transformed_function"] + assert function() == transformed() if __name__ == "__main__": diff --git a/numba_rvsdg/tests/test_figures.py b/numba_rvsdg/tests/test_figures.py index 9476634..7a3a569 100644 --- a/numba_rvsdg/tests/test_figures.py +++ b/numba_rvsdg/tests/test_figures.py @@ -145,34 +145,34 @@ end: 22 synth_asign_block_0: type: synth_asign - variable_assignment: {'control_var_0': 0} + variable_assignment: {'__scfg_control_var_0__': 0} synth_asign_block_1: type: synth_asign - variable_assignment: {'control_var_0': 1} + variable_assignment: {'__scfg_control_var_0__': 1} synth_asign_block_2: type: synth_asign - variable_assignment: {'control_var_0': 0, 'backedge_var_0': 1} + variable_assignment: {'__scfg_control_var_0__': 0, '__scfg_backedge_var_0__': 1} synth_asign_block_3: type: synth_asign - variable_assignment: {'backedge_var_0': 0, 'control_var_0': 1} + variable_assignment: {'__scfg_backedge_var_0__': 0, '__scfg_control_var_0__': 1} synth_asign_block_4: type: synth_asign - variable_assignment: {'control_var_0': 1, 'backedge_var_0': 1} + variable_assignment: {'__scfg_control_var_0__': 1, '__scfg_backedge_var_0__': 1} synth_asign_block_5: type: synth_asign - variable_assignment: {'backedge_var_0': 0, 'control_var_0': 0} + variable_assignment: {'__scfg_backedge_var_0__': 0, '__scfg_control_var_0__': 0} synth_exit_block_0: type: synth_exit_branch branch_value_table: {0: 'branch_region_2', 1: 'branch_region_3'} - variable: control_var_0 + variable: __scfg_control_var_0__ synth_exit_latch_block_0: type: synth_exit_latch branch_value_table: {1: 'synth_exit_block_0', 0: 'head_region_2'} - variable: backedge_var_0 + variable: __scfg_backedge_var_0__ synth_head_block_0: type: synth_head branch_value_table: {0: 'branch_region_4', 1: 'branch_region_5'} - variable: control_var_0 + variable: __scfg_control_var_0__ synth_tail_block_0: type: synth_tail synth_tail_block_1: @@ -345,19 +345,19 @@ end: 20 synth_asign_block_0: type: synth_asign - variable_assignment: {'control_var_0': 0} + variable_assignment: {'__scfg_control_var_0__': 0} synth_asign_block_1: type: synth_asign - variable_assignment: {'control_var_0': 1} + variable_assignment: {'__scfg_control_var_0__': 1} synth_asign_block_2: type: synth_asign - variable_assignment: {'control_var_0': 2} + variable_assignment: {'__scfg_control_var_0__': 2} synth_fill_block_0: type: synth_fill synth_head_block_0: type: synth_head branch_value_table: {0: 'branch_region_4', 2: 'branch_region_4', 1: 'branch_region_5'} # noqa - variable: control_var_0 + variable: __scfg_control_var_0__ synth_tail_block_0: type: synth_tail tail_region_0: diff --git a/numba_rvsdg/tests/test_rendering.py b/numba_rvsdg/tests/test_rendering.py index 1149fae..7f21d63 100644 --- a/numba_rvsdg/tests/test_rendering.py +++ b/numba_rvsdg/tests/test_rendering.py @@ -48,7 +48,7 @@ color=green label="branch_region_0 jump targets: ('tail_region_0',) back edges: ()" - synth_asign_block_0 [label="synth_asign_block_0\lcontrol_var_0 = 0 + synth_asign_block_0 [label="synth_asign_block_0\l__scfg_control_var_0__ = 0 jump targets: ('tail_region_0',) back edges: ()" shape=rect] } @@ -56,7 +56,7 @@ color=green label="branch_region_1 jump targets: ('tail_region_0',) back edges: ()" - synth_asign_block_1 [label="synth_asign_block_1\lcontrol_var_0 = 1 + synth_asign_block_1 [label="synth_asign_block_1\l__scfg_control_var_0__ = 1 jump targets: ('tail_region_0',) back edges: ()" shape=rect] } @@ -75,7 +75,7 @@ color=red label="head_region_1 jump targets: ('branch_region_2', 'branch_region_3') back edges: ()" - synth_head_block_0 [label="synth_head_block_0\lvariable: control_var_0\l0=>branch_region_2\l1=>branch_region_3 + synth_head_block_0 [label="synth_head_block_0\lvariable: __scfg_control_var_0__\l0=>branch_region_2\l1=>branch_region_3 jump targets: ('branch_region_2', 'branch_region_3') back edges: ()" shape=rect] } @@ -98,7 +98,7 @@ color=green label="branch_region_4 jump targets: ('tail_region_2',) back edges: ()" - synth_asign_block_2 [label="synth_asign_block_2\lbackedge_var_0 = 0\lcontrol_var_0 = 1 + synth_asign_block_2 [label="synth_asign_block_2\l__scfg_backedge_var_0__ = 0\l__scfg_control_var_0__ = 1 jump targets: ('tail_region_2',) back edges: ()" shape=rect] } @@ -106,7 +106,7 @@ color=green label="branch_region_5 jump targets: ('tail_region_2',) back edges: ()" - synth_asign_block_3 [label="synth_asign_block_3\lbackedge_var_0 = 1 + synth_asign_block_3 [label="synth_asign_block_3\l__scfg_backedge_var_0__ = 1 jump targets: ('tail_region_2',) back edges: ()" shape=rect] } @@ -129,7 +129,7 @@ 4 [label="4\l jump targets: ('synth_asign_block_4',) back edges: ()" shape=rect] - synth_asign_block_4 [label="synth_asign_block_4\lbackedge_var_0 = 0\lcontrol_var_0 = 0 + synth_asign_block_4 [label="synth_asign_block_4\l__scfg_backedge_var_0__ = 0\l__scfg_control_var_0__ = 0 jump targets: ('tail_region_1',) back edges: ()" shape=rect] } @@ -137,7 +137,7 @@ color=purple label="tail_region_1 jump targets: ('5',) back edges: ()" - synth_exit_latch_block_0 [label="synth_exit_latch_block_0\lvariable: backedge_var_0\l1=>5\l0=>head_region_1 + synth_exit_latch_block_0 [label="synth_exit_latch_block_0\lvariable: __scfg_backedge_var_0__\l1=>5\l0=>head_region_1 jump targets: ('5',) back edges: ('head_region_1',)" shape=rect] }