diff --git a/numba_rvsdg/core/datastructures/basic_block.py b/numba_rvsdg/core/datastructures/basic_block.py index 263e09cc..393b1057 100644 --- a/numba_rvsdg/core/datastructures/basic_block.py +++ b/numba_rvsdg/core/datastructures/basic_block.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, replace from numba_rvsdg.core.utils import _next_inst_offset +from numba_rvsdg.core.datastructures import block_names @dataclass(frozen=True) @@ -384,3 +385,19 @@ def replace_exiting(self, new_exiting): The new exiting block of the region represented by the RegionBlock. """ object.__setattr__(self, "exiting", new_exiting) + + +block_type_names = { + block_names.BASIC: BasicBlock, + block_names.PYTHON_BYTECODE: PythonBytecodeBlock, + block_names.SYNTH_HEAD: SyntheticHead, + block_names.SYNTH_BRANCH: SyntheticBranch, + block_names.SYNTH_TAIL: SyntheticTail, + block_names.SYNTH_EXIT: SyntheticExit, + block_names.SYNTH_ASSIGN: SyntheticAssignment, + block_names.SYNTH_RETURN: SyntheticReturn, + block_names.SYNTH_EXIT_LATCH: SyntheticExitingLatch, + block_names.SYNTH_EXIT_BRANCH: SyntheticExitBranch, + block_names.SYNTH_FILL: SyntheticFill, + block_names.REGION: RegionBlock, +} diff --git a/numba_rvsdg/core/datastructures/block_names.py b/numba_rvsdg/core/datastructures/block_names.py index 07ac40dc..7226b7d5 100644 --- a/numba_rvsdg/core/datastructures/block_names.py +++ b/numba_rvsdg/core/datastructures/block_names.py @@ -10,3 +10,21 @@ SYNTH_RETURN = "synth_return" SYNTH_EXIT_LATCH = "synth_exit_latch" SYNTH_FILL = "synth_fill" +SYNTH_EXIT_BRANCH = "synth_exit_branch" + +REGION = "region" + +block_types = { + BASIC, + PYTHON_BYTECODE, + SYNTH_HEAD, + SYNTH_BRANCH, + SYNTH_TAIL, + SYNTH_EXIT, + SYNTH_ASSIGN, + SYNTH_RETURN, + SYNTH_EXIT_LATCH, + SYNTH_EXIT_BRANCH, + SYNTH_FILL, + REGION, +} diff --git a/numba_rvsdg/core/datastructures/scfg.py b/numba_rvsdg/core/datastructures/scfg.py index 16f96078..4e5276d0 100644 --- a/numba_rvsdg/core/datastructures/scfg.py +++ b/numba_rvsdg/core/datastructures/scfg.py @@ -1,6 +1,6 @@ import dis import yaml -from textwrap import dedent +from textwrap import indent from typing import Set, Tuple, Dict, List, Iterator from dataclasses import dataclass, field from collections import deque @@ -15,9 +15,18 @@ SyntheticTail, SyntheticReturn, SyntheticFill, + PythonBytecodeBlock, RegionBlock, + SyntheticBranch, + block_type_names, +) +from numba_rvsdg.core.datastructures.block_names import ( + block_types, + SYNTH_TAIL, + SYNTH_EXIT, + SYNTH_ASSIGN, + SYNTH_RETURN, ) -from numba_rvsdg.core.datastructures import block_names @dataclass(frozen=True) @@ -603,10 +612,8 @@ def insert_block_and_control_blocks( # Need to create synthetic assignments for each arc from a # predecessors to a successor and insert it between the predecessor # and the newly created block - for s in set(jt).intersection(successors): - synth_assign = self.name_gen.new_block_name( - block_names.SYNTH_ASSIGN - ) + for s in sorted(set(jt).intersection(successors)): + synth_assign = self.name_gen.new_block_name(SYNTH_ASSIGN) variable_assignment = {} variable_assignment[branch_variable] = branch_variable_value synth_assign_block = SyntheticAssignment( @@ -652,9 +659,7 @@ def join_returns(self): ] # close if more than one is found if len(return_nodes) > 1: - return_solo_name = self.name_gen.new_block_name( - block_names.SYNTH_RETURN - ) + return_solo_name = self.name_gen.new_block_name(SYNTH_RETURN) self.insert_SyntheticReturn( return_solo_name, return_nodes, tuple() ) @@ -685,29 +690,21 @@ def join_tails_and_exits(self, tails: Set[str], exits: Set[str]): if len(tails) == 1 and len(exits) == 2: # join only exits solo_tail_name = next(iter(tails)) - solo_exit_name = self.name_gen.new_block_name( - block_names.SYNTH_EXIT - ) + solo_exit_name = self.name_gen.new_block_name(SYNTH_EXIT) self.insert_SyntheticExit(solo_exit_name, tails, exits) return solo_tail_name, solo_exit_name if len(tails) >= 2 and len(exits) == 1: # join only tails - solo_tail_name = self.name_gen.new_block_name( - block_names.SYNTH_TAIL - ) + solo_tail_name = self.name_gen.new_block_name(SYNTH_TAIL) solo_exit_name = next(iter(exits)) self.insert_SyntheticTail(solo_tail_name, tails, exits) return solo_tail_name, solo_exit_name if len(tails) >= 2 and len(exits) >= 2: # join both tails and exits - solo_tail_name = self.name_gen.new_block_name( - block_names.SYNTH_TAIL - ) - solo_exit_name = self.name_gen.new_block_name( - block_names.SYNTH_EXIT - ) + solo_tail_name = self.name_gen.new_block_name(SYNTH_TAIL) + solo_exit_name = self.name_gen.new_block_name(SYNTH_EXIT) self.insert_SyntheticTail(solo_tail_name, tails, exits) self.insert_SyntheticExit(solo_exit_name, {solo_tail_name}, exits) return solo_tail_name, solo_exit_name @@ -730,15 +727,146 @@ def bcmap_from_bytecode(bc: dis.Bytecode): """ return {inst.offset: inst for inst in bc} + def view(self, name: str = None): + """View the current SCFG as a external PDF file. + + This method internally creates a SCFGRenderer corresponding to + the current state of SCFG and calls it's view method to view the + graph as a graphviz generated external PDF file. + + Parameters + ---------- + name: str + Name to be given to the external graphviz generated PDF file. + """ + from numba_rvsdg.rendering.rendering import SCFGRenderer + + SCFGRenderer(self).view(name) + @staticmethod - def from_yaml(yaml_string): + def from_yaml(yaml_string: str): """Static method that creates an SCFG object from a YAML representation. This method takes a YAML string representing the control flow graph and returns an SCFG object and a dictionary of block names in YAML string - corresponding to thier representation/unique name IDs in the SCFG. + corresponding to their representation/unique name IDs in the SCFG. + + Internally forwards the `yaml_string` to `SCFGIO.from_yaml()` + helper method. + + Parameters + ---------- + yaml: str + The input YAML string from which the SCFG is to be constructed. + + Return + ------ + scfg: SCFG + The corresponding SCFG created using the YAML representation. + block_dict: Dict[str, str] + Dictionary of block names in YAML string corresponding to their + representation/unique name IDs in the SCFG. + + See also + -------- + numba_rvsdg.core.datastructures.scfg.SCFGIO.from_yaml() + """ + return SCFGIO.from_yaml(yaml_string) + + @staticmethod + def from_dict(graph_dict: dict): + """Static method that creates an SCFG object from a dictionary + representation. + + This method takes a dictionary (graph_dict) + representing the control flow graph and returns an SCFG + object and a dictionary of block names. The input dictionary + should have block indices as keys and dictionaries of block + attributes as values. + + Internally forwards the `graph_dict` to `SCFGIO.from_dict()` + helper method. + + Parameters + ---------- + graph_dict: dict + The input dictionary from which the SCFG is to be constructed. + + Return + ------ + scfg: SCFG + The corresponding SCFG created using the dictionary representation. + block_dict: Dict[str, str] + Dictionary of block names in YAML string corresponding to their + representation/unique name IDs in the SCFG. + + See also + -------- + numba_rvsdg.core.datastructures.scfg.SCFGIO.from_dict() + """ + return SCFGIO.from_dict(graph_dict) + + def to_yaml(self): + """Converts the SCFG object to a YAML string representation. + + The method returns a YAML string representing the control + flow graph. It iterates over the graph dictionary and + generates YAML entries for each block, including jump + targets and backedges. + + Internally calls the `SCFGIO.to_yaml()` helper method on + current `SCFG` object. + + Returns + ------- + yaml: str + A YAML string representing the SCFG. + + See also + -------- + numba_rvsdg.core.datastructures.scfg.SCFGIO.to_yaml() + """ + return SCFGIO.to_yaml(self) + + def to_dict(self): + """Converts the SCFG object to a dictionary representation. + + This method returns a dictionary representing the control flow + graph. It iterates over the graph dictionary and generates a + dictionary entry for each block, including jump targets and + backedges if present. + + Internally calls the `SCFGIO.to_dict()` helper method on + current `SCFG` object. + + Returns + ------- + graph_dict: Dict[Dict[...]] + A dictionary representing the SCFG. + + See also + -------- + numba_rvsdg.core.datastructures.scfg.SCFGIO.to_dict() + """ + return SCFGIO.to_dict(self) + + +class SCFGIO: + """Helper class for `SCFG` object transformation to and from various + other formats. Currently supports YAML and dictionary format. + """ + + @staticmethod + def from_yaml(yaml_string: str): + """Static helper method that creates an SCFG object from a YAML + representation. + + This method takes a YAML string + representing the control flow graph and returns an SCFG + object and a dictionary of block names in YAML string + corresponding to their representation/unique name IDs in the SCFG. Parameters ---------- @@ -759,7 +887,7 @@ def from_yaml(yaml_string): @staticmethod def from_dict(graph_dict: dict): - """Static method that creates an SCFG object from a dictionary + """Static helper method that creates an SCFG object from a dictionary representation. This method takes a dictionary (graph_dict) @@ -777,99 +905,290 @@ def from_dict(graph_dict: dict): ------ scfg: SCFG The corresponding SCFG created using the dictionary representation. - block_dict: Dict[str, str] + block_ref_dict: Dict[str, str] Dictionary of block names in YAML string corresponding to their representation/unique name IDs in the SCFG. """ - scfg_graph = {} + block_ref_dict = {} + for key, block in graph_dict["blocks"].items(): + assert block["type"] in block_types + block_ref_dict[key] = key + + outer_graph = SCFGIO.find_outer_graph(graph_dict) + assert len(outer_graph) > 0 + name_gen = NameGenerator() - block_dict = {} - for index in graph_dict.keys(): - block_dict[index] = name_gen.new_block_name(block_names.BASIC) - for index, attributes in graph_dict.items(): - jump_targets = attributes["jt"] - backedges = attributes.get("be", ()) - name = block_dict[index] - block = BasicBlock( - name=name, - backedges=tuple(block_dict[idx] for idx in backedges), - _jump_targets=tuple(block_dict[idx] for idx in jump_targets), + scfg = SCFGIO.make_scfg( + graph_dict, outer_graph, block_ref_dict, name_gen + ) + + return scfg, block_ref_dict + + @staticmethod + def make_scfg( + graph_dict, + curr_heads: set, + block_ref_dict, + name_gen, + exiting: str = None, + ): + """Helper method for building a single 'level' of the hierarchical + structure in an `SCFG` graph at a time. Recursively calls itself + to build the entire graph. + + Parameters + ---------- + graph_dict: dict + The input dictionary from which the SCFG is to be constructed. + curr_heads: set + The set of blocks to start iterating from. + block_ref_dict: Dict[str, str] + Dictionary of block names in YAML string corresponding to their + representation/unique name IDs in the SCFG. + name_gen: NameGenerator + The corresponding `NameGenerator` object for the `SCFG` object + to be created. + exiting: str + The exiting node for the current region being iterated. + + Return + ------ + scfg: SCFG + The corresponding SCFG created using the dictionary representation. + """ + blocks = graph_dict["blocks"] + edges = graph_dict["edges"] + backedges = graph_dict["backedges"] + if backedges is None: + backedges = {} + + scfg_graph = {} + seen = set() + queue = curr_heads + + while queue: + current_name = queue.pop() + if current_name in seen: + continue + seen.add(current_name) + + ( + block_info, + block_type, + block_edges, + block_backedges, + ) = SCFGIO.extract_block_info( + blocks, current_name, block_ref_dict, edges, backedges ) - scfg_graph[name] = block + + if block_type == "region": + block_info["subregion"] = SCFGIO.make_scfg( + graph_dict, + {block_info["header"]}, + block_ref_dict, + name_gen, + block_info["exiting"], + ) + block_info.pop("contains") + + block_class = block_type_names[block_type] + block = block_class( + name=current_name, + backedges=block_backedges, + _jump_targets=block_edges, + **block_info, + ) + + scfg_graph[current_name] = block + if current_name != exiting: + queue.update(edges[current_name]) + scfg = SCFG(scfg_graph, name_gen=name_gen) - return scfg, block_dict + return scfg - def to_yaml(self): - """Converts the SCFG object to a YAML string representation. + @staticmethod + def to_yaml(scfg): + """Helper method to convert the SCFG object to a YAML + string representation. The method returns a YAML string representing the control flow graph. It iterates over the graph dictionary and generates YAML entries for each block, including jump targets and backedges. + Parameters + ---------- + scfg: SCFG + The `SCFG` object to be transformed. + Returns ------- yaml: str A YAML string representing the SCFG. """ # Convert to yaml - scfg_graph = self.graph - yaml_string = """""" + ys = "" - for key, value in scfg_graph.items(): - jump_targets = [i for i in value._jump_targets] - jump_targets = str(jump_targets).replace("'", '"') - back_edges = [i for i in value.backedges] - jump_target_str = f""" - "{key}": - jt: {jump_targets}""" + graph_dict = SCFGIO.to_dict(scfg) - if back_edges: - back_edges = str(back_edges).replace("'", '"') - jump_target_str += f""" - be: {back_edges}""" - yaml_string += dedent(jump_target_str) + blocks = graph_dict["blocks"] + edges = graph_dict["edges"] + backedges = graph_dict["backedges"] - return yaml_string + ys += "\nblocks:\n" + for b in sorted(blocks): + ys += indent(f"'{b}':\n", " " * 8) + for k, v in blocks[b].items(): + ys += indent(f"{k}: {v}\n", " " * 12) - def to_dict(self): - """Converts the SCFG object to a dictionary representation. + ys += "\nedges:\n" + for b in sorted(blocks): + ys += indent(f"'{b}': {edges[b]}\n", " " * 8) + + ys += "\nbackedges:\n" + for b in sorted(blocks): + if backedges[b]: + ys += indent(f"'{b}': {backedges[b]}\n", " " * 8) + return ys + + @staticmethod + def to_dict(scfg): + """Helper method to convert the SCFG object to a dictionary + representation. This method returns a dictionary representing the control flow graph. It iterates over the graph dictionary and generates a dictionary entry for each block, including jump targets and backedges if present. + Parameters + ---------- + scfg: SCFG + The `SCFG` object to be transformed. + Returns ------- graph_dict: Dict[Dict[...]] A dictionary representing the SCFG. """ - scfg_graph = self.graph - graph_dict = {} - for key, value in scfg_graph.items(): - curr_dict = {} - curr_dict["jt"] = [i for i in value._jump_targets] - if value.backedges: - curr_dict["be"] = [i for i in value.backedges] - graph_dict[key] = curr_dict + blocks, edges, backedges = {}, {}, {} + + def reverse_lookup(value: type): + for k, v in block_type_names.items(): + if v == value: + return k + else: + raise TypeError("Block type not found.") + + seen = set() + q = set() + # Order of elements doesn't matter since they're going to + # be sorted at the end. + q.update(scfg.graph.items()) + + while q: + key, value = q.pop() + if key in seen: + continue + seen.add(key) + + block_type = reverse_lookup(type(value)) + blocks[key] = {"type": block_type} + if isinstance(value, RegionBlock): + q.update(value.subregion.graph.items()) + blocks[key]["kind"] = value.kind + blocks[key]["contains"] = sorted( + [idx.name for idx in value.subregion.graph.values()] + ) + blocks[key]["header"] = value.header + blocks[key]["exiting"] = value.exiting + blocks[key]["parent_region"] = value.parent_region.name + elif isinstance(value, SyntheticBranch): + blocks[key]["branch_value_table"] = value.branch_value_table + blocks[key]["variable"] = value.variable + elif isinstance(value, SyntheticAssignment): + blocks[key]["variable_assignment"] = value.variable_assignment + elif isinstance(value, PythonBytecodeBlock): + blocks[key]["begin"] = value.begin + blocks[key]["end"] = value.end + edges[key] = sorted([i for i in value._jump_targets]) + backedges[key] = sorted([i for i in value.backedges]) + + graph_dict = {"blocks": blocks, "edges": edges, "backedges": backedges} + return graph_dict - def view(self, name: str = None): - """View the current SCFG as a external PDF file. + @staticmethod + def find_outer_graph(graph_dict: dict): + """Helper method to find the outermost graph components + of an `SCFG` object. (i.e. Components that aren't + contained in any other region) - This method internally creates a SCFGRenderer corresponding to - the current state of SCFG and calls it's view method to view the - graph as a graphviz generated external PDF file. + Parameters + ---------- + graph_dict: dict + The input dictionary from which the SCFG is to be constructed. + + Return + ------ + outer_blocks: set[str] + Set of all the block names that lie in the outer most graph or + aren't a part of any region. + """ + blocks = graph_dict["blocks"] + + outer_blocks = set(blocks.keys()) + for _, block_data in blocks.items(): + if block_data.get("contains"): + outer_blocks.difference_update(block_data["contains"]) + + return outer_blocks + + @staticmethod + def extract_block_info( + blocks, current_name, block_ref_dict, edges, backedges + ): + """Helper method to extract information from various components of + an `SCFG` graph. Parameters ---------- - name: str - Name to be given to the external graphviz generated PDF file. + blocks: Dict[str, dict] + Dictionary containing all the blocks info + current_name: str + Name of the block whose information is to be extracted. + block_ref_dict: Dict[str, str] + Dictionary of block names in YAML string corresponding to their + representation/unique name IDs in the SCFG. + edges: Dict[str, list[str]] + Dictionary representing the edges of the graph. + backedges: Dict[str, list[str]] + Dictionary representing the backedges of the graph. + + Return + ------ + block_info: Dict[str, Any] + Dictionary containing information about the block. + block_type: str + String representing the type of block. + block_edges: List[str] + List of edges of the requested block. + block_backedges: List[str] + List of backedges of the requested block. """ - from numba_rvsdg.rendering.rendering import SCFGRenderer + block_info = blocks[current_name].copy() + block_edges = tuple(block_ref_dict[idx] for idx in edges[current_name]) - SCFGRenderer(self).view(name) + if backedges.get(current_name): + block_backedges = tuple( + block_ref_dict[idx] for idx in backedges[current_name] + ) + else: + block_backedges = () + + block_type = block_info.pop("type") + + return block_info, block_type, block_edges, block_backedges class AbstractGraphView(Mapping): diff --git a/numba_rvsdg/tests/test_fig3.py b/numba_rvsdg/tests/test_fig3.py deleted file mode 100644 index 75b090ac..00000000 --- a/numba_rvsdg/tests/test_fig3.py +++ /dev/null @@ -1,42 +0,0 @@ -# Figure 3 of the paper -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow -from numba_rvsdg.core.datastructures.flow_info import FlowInfo -from numba_rvsdg.rendering.rendering import render_flow - -# import logging -# logging.basicConfig(level=logging.DEBUG) - - -def make_flow(): - # flowinfo = FlowInfo() - import dis - - # fake bytecode just good enough for FlowInfo - bc = [ - dis.Instruction("OP", 1, None, None, "", 0, None, False), - dis.Instruction("POP_JUMP_IF_TRUE", 2, None, 12, "", 2, None, False), - # label 4 - dis.Instruction("OP", 1, None, None, "", 4, None, False), - dis.Instruction("POP_JUMP_IF_TRUE", 2, None, 12, "", 6, None, False), - dis.Instruction("OP", 1, None, None, "", 8, None, False), - dis.Instruction("JUMP_ABSOLUTE", 2, None, 20, "", 10, None, False), - # label 12 - dis.Instruction("OP", 1, None, None, "", 12, None, False), - dis.Instruction("POP_JUMP_IF_TRUE", 2, None, 4, "", 14, None, False), - dis.Instruction("OP", 1, None, None, "", 16, None, False), - dis.Instruction("JUMP_ABSOLUTE", 2, None, 20, "", 18, None, False), - # label 20 - dis.Instruction("RETURN_VALUE", 1, None, None, "", 20, None, False), - ] - flow = FlowInfo.from_bytecode(bc) - scfg = flow.build_basicblocks() - return ByteFlow(bc=bc, scfg=scfg) - - -def test_fig3(): - f = make_flow() - f.restructure() - - -if __name__ == "__main__": - render_flow(make_flow()) diff --git a/numba_rvsdg/tests/test_fig4.py b/numba_rvsdg/tests/test_fig4.py deleted file mode 100644 index 7d9a046e..00000000 --- a/numba_rvsdg/tests/test_fig4.py +++ /dev/null @@ -1,41 +0,0 @@ -# Figure 3 of the paper -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow -from numba_rvsdg.core.datastructures.flow_info import FlowInfo -from numba_rvsdg.rendering.rendering import render_flow - -# import logging -# logging.basicConfig(level=logging.DEBUG) - - -def make_flow(): - # flowinfo = FlowInfo() - import dis - - # fake bytecode just good enough for FlowInfo - bc = [ - dis.Instruction("OP", 1, None, None, "", 0, None, False), - dis.Instruction("POP_JUMP_IF_TRUE", 2, None, 14, "", 2, None, False), - # label 4 - dis.Instruction("OP", 1, None, None, "", 4, None, False), - dis.Instruction("POP_JUMP_IF_TRUE", 2, None, 12, "", 6, None, False), - dis.Instruction("OP", 1, None, None, "", 8, None, False), - dis.Instruction("JUMP_ABSOLUTE", 2, None, 18, "", 10, None, False), - # label 12 - dis.Instruction("OP", 1, None, None, "", 12, None, False), - dis.Instruction("OP", 2, None, 4, "", 14, None, False), - dis.Instruction("JUMP_ABSOLUTE", 2, None, 18, "", 16, None, False), - # label 18 - dis.Instruction("RETURN_VALUE", 1, None, None, "", 18, None, False), - ] - flow = FlowInfo.from_bytecode(bc) - scfg = flow.build_basicblocks() - return ByteFlow(bc=bc, scfg=scfg) - - -def test_fig4(): - f = make_flow() - f.restructure() - - -if __name__ == "__main__": - render_flow(make_flow()) diff --git a/numba_rvsdg/tests/test_figures.py b/numba_rvsdg/tests/test_figures.py new file mode 100644 index 00000000..82419f1d --- /dev/null +++ b/numba_rvsdg/tests/test_figures.py @@ -0,0 +1,479 @@ +from numba_rvsdg.core.datastructures.byte_flow import ByteFlow +from numba_rvsdg.core.datastructures.flow_info import FlowInfo +from numba_rvsdg.core.datastructures.scfg import SCFG +from numba_rvsdg.tests.test_utils import SCFGComparator +import dis + +fig_3_yaml = """ +blocks: + branch_region_0: + type: region + kind: branch + contains: ['synth_asign_block_0'] + header: synth_asign_block_0 + exiting: synth_asign_block_0 + parent_region: meta_region_0 + branch_region_1: + type: region + kind: branch + contains: ['synth_asign_block_1'] + header: synth_asign_block_1 + exiting: synth_asign_block_1 + parent_region: meta_region_0 + branch_region_2: + type: region + kind: branch + contains: ['python_bytecode_block_2'] + header: python_bytecode_block_2 + exiting: python_bytecode_block_2 + parent_region: tail_region_0 + branch_region_3: + type: region + kind: branch + contains: ['python_bytecode_block_4'] + header: python_bytecode_block_4 + exiting: python_bytecode_block_4 + parent_region: tail_region_0 + branch_region_4: + type: region + kind: branch + contains: ['branch_region_6', 'branch_region_7', 'head_region_3', 'tail_region_3'] # noqa + header: head_region_3 + exiting: tail_region_3 + parent_region: loop_region_0 + branch_region_5: + type: region + kind: branch + contains: ['branch_region_8', 'branch_region_9', 'head_region_4', 'tail_region_4'] # noqa + header: head_region_4 + exiting: tail_region_4 + parent_region: loop_region_0 + branch_region_6: + type: region + kind: branch + contains: ['synth_asign_block_2'] + header: synth_asign_block_2 + exiting: synth_asign_block_2 + parent_region: branch_region_4 + branch_region_7: + type: region + kind: branch + contains: ['synth_asign_block_3'] + header: synth_asign_block_3 + exiting: synth_asign_block_3 + parent_region: branch_region_4 + branch_region_8: + type: region + kind: branch + contains: ['synth_asign_block_4'] + header: synth_asign_block_4 + exiting: synth_asign_block_4 + parent_region: branch_region_5 + branch_region_9: + type: region + kind: branch + contains: ['synth_asign_block_5'] + header: synth_asign_block_5 + exiting: synth_asign_block_5 + parent_region: branch_region_5 + head_region_0: + type: region + kind: head + contains: ['python_bytecode_block_0'] + header: python_bytecode_block_0 + exiting: python_bytecode_block_0 + parent_region: meta_region_0 + head_region_1: + type: region + kind: head + contains: ['loop_region_0', 'synth_exit_block_0'] + header: loop_region_0 + exiting: synth_exit_block_0 + parent_region: tail_region_0 + head_region_2: + type: region + kind: head + contains: ['synth_head_block_0'] + header: synth_head_block_0 + exiting: synth_head_block_0 + parent_region: loop_region_0 + head_region_3: + type: region + kind: head + contains: ['python_bytecode_block_1'] + header: python_bytecode_block_1 + exiting: python_bytecode_block_1 + parent_region: branch_region_4 + head_region_4: + type: region + kind: head + contains: ['python_bytecode_block_3'] + header: python_bytecode_block_3 + exiting: python_bytecode_block_3 + parent_region: branch_region_5 + loop_region_0: + type: region + kind: loop + contains: ['branch_region_4', 'branch_region_5', 'head_region_2', 'tail_region_2'] + header: head_region_2 + exiting: tail_region_2 + parent_region: head_region_1 + python_bytecode_block_0: + type: python_bytecode + begin: 0 + end: 4 + python_bytecode_block_1: + type: python_bytecode + begin: 4 + end: 8 + python_bytecode_block_2: + type: python_bytecode + begin: 8 + end: 12 + python_bytecode_block_3: + type: python_bytecode + begin: 12 + end: 16 + python_bytecode_block_4: + type: python_bytecode + begin: 16 + end: 20 + python_bytecode_block_5: + type: python_bytecode + begin: 20 + end: 22 + synth_asign_block_0: + type: synth_asign + variable_assignment: {'control_var_0': 0} + synth_asign_block_1: + type: synth_asign + variable_assignment: {'control_var_0': 1} + synth_asign_block_2: + type: synth_asign + variable_assignment: {'control_var_0': 0, 'backedge_var_0': 1} + synth_asign_block_3: + type: synth_asign + variable_assignment: {'backedge_var_0': 0, 'control_var_0': 1} + synth_asign_block_4: + type: synth_asign + variable_assignment: {'control_var_0': 1, 'backedge_var_0': 1} + synth_asign_block_5: + type: synth_asign + variable_assignment: {'backedge_var_0': 0, '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 + 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 + synth_head_block_0: + type: synth_head + branch_value_table: {0: 'branch_region_4', 1: 'branch_region_5'} + variable: control_var_0 + synth_tail_block_0: + type: synth_tail + synth_tail_block_1: + type: synth_tail + tail_region_0: + type: region + kind: tail + contains: ['branch_region_2', 'branch_region_3', 'head_region_1', 'tail_region_1'] + header: head_region_1 + exiting: tail_region_1 + parent_region: meta_region_0 + tail_region_1: + type: region + kind: tail + contains: ['python_bytecode_block_5'] + header: python_bytecode_block_5 + exiting: python_bytecode_block_5 + parent_region: tail_region_0 + tail_region_2: + type: region + kind: tail + contains: ['synth_exit_latch_block_0'] + header: synth_exit_latch_block_0 + exiting: synth_exit_latch_block_0 + parent_region: loop_region_0 + tail_region_3: + type: region + kind: tail + contains: ['synth_tail_block_0'] + header: synth_tail_block_0 + exiting: synth_tail_block_0 + parent_region: branch_region_4 + tail_region_4: + type: region + kind: tail + contains: ['synth_tail_block_1'] + header: synth_tail_block_1 + exiting: synth_tail_block_1 + parent_region: branch_region_5 +edges: + branch_region_0: ['tail_region_0'] + branch_region_1: ['tail_region_0'] + branch_region_2: ['tail_region_1'] + branch_region_3: ['tail_region_1'] + branch_region_4: ['tail_region_2'] + branch_region_5: ['tail_region_2'] + branch_region_6: ['tail_region_3'] + branch_region_7: ['tail_region_3'] + branch_region_8: ['tail_region_4'] + branch_region_9: ['tail_region_4'] + head_region_0: ['branch_region_0', 'branch_region_1'] + head_region_1: ['branch_region_2', 'branch_region_3'] + head_region_2: ['branch_region_4', 'branch_region_5'] + head_region_3: ['branch_region_6', 'branch_region_7'] + head_region_4: ['branch_region_8', 'branch_region_9'] + loop_region_0: ['synth_exit_block_0'] + python_bytecode_block_0: ['branch_region_0', 'branch_region_1'] + python_bytecode_block_1: ['branch_region_6', 'branch_region_7'] + python_bytecode_block_2: ['tail_region_1'] + python_bytecode_block_3: ['branch_region_8', 'branch_region_9'] + python_bytecode_block_4: ['tail_region_1'] + python_bytecode_block_5: [] + synth_asign_block_0: ['tail_region_0'] + synth_asign_block_1: ['tail_region_0'] + synth_asign_block_2: ['tail_region_3'] + synth_asign_block_3: ['tail_region_3'] + synth_asign_block_4: ['tail_region_4'] + synth_asign_block_5: ['tail_region_4'] + synth_exit_block_0: ['branch_region_2', 'branch_region_3'] + synth_exit_latch_block_0: ['head_region_2', 'synth_exit_block_0'] + synth_head_block_0: ['branch_region_4', 'branch_region_5'] + synth_tail_block_0: ['tail_region_2'] + synth_tail_block_1: ['tail_region_2'] + tail_region_0: [] + tail_region_1: [] + tail_region_2: ['synth_exit_block_0'] + tail_region_3: ['tail_region_2'] + tail_region_4: ['tail_region_2'] +backedges: + synth_exit_latch_block_0: ['head_region_2']""" + +fig_4_yaml = """ +blocks: + branch_region_0: + type: region + kind: branch + contains: ['branch_region_2', 'branch_region_3', 'head_region_1', 'tail_region_1'] # noqa + header: head_region_1 + exiting: tail_region_1 + parent_region: meta_region_0 + branch_region_1: + type: region + kind: branch + contains: ['synth_asign_block_0'] + header: synth_asign_block_0 + exiting: synth_asign_block_0 + parent_region: meta_region_0 + branch_region_2: + type: region + kind: branch + contains: ['python_bytecode_block_2', 'synth_asign_block_1'] + header: python_bytecode_block_2 + exiting: synth_asign_block_1 + parent_region: branch_region_0 + branch_region_3: + type: region + kind: branch + contains: ['python_bytecode_block_3', 'synth_asign_block_2'] + header: python_bytecode_block_3 + exiting: synth_asign_block_2 + parent_region: branch_region_0 + branch_region_4: + type: region + kind: branch + contains: ['python_bytecode_block_4'] + header: python_bytecode_block_4 + exiting: python_bytecode_block_4 + parent_region: tail_region_0 + branch_region_5: + type: region + kind: branch + contains: ['synth_fill_block_0'] + header: synth_fill_block_0 + exiting: synth_fill_block_0 + parent_region: tail_region_0 + head_region_0: + type: region + kind: head + contains: ['python_bytecode_block_0'] + header: python_bytecode_block_0 + exiting: python_bytecode_block_0 + parent_region: meta_region_0 + head_region_1: + type: region + kind: head + contains: ['python_bytecode_block_1'] + header: python_bytecode_block_1 + exiting: python_bytecode_block_1 + parent_region: branch_region_0 + head_region_2: + type: region + kind: head + contains: ['synth_head_block_0'] + header: synth_head_block_0 + exiting: synth_head_block_0 + parent_region: tail_region_0 + python_bytecode_block_0: + type: python_bytecode + begin: 0 + end: 4 + python_bytecode_block_1: + type: python_bytecode + begin: 4 + end: 8 + python_bytecode_block_2: + type: python_bytecode + begin: 8 + end: 12 + python_bytecode_block_3: + type: python_bytecode + begin: 12 + end: 14 + python_bytecode_block_4: + type: python_bytecode + begin: 14 + end: 18 + python_bytecode_block_5: + type: python_bytecode + begin: 18 + end: 20 + synth_asign_block_0: + type: synth_asign + variable_assignment: {'control_var_0': 0} + synth_asign_block_1: + type: synth_asign + variable_assignment: {'control_var_0': 1} + synth_asign_block_2: + type: synth_asign + variable_assignment: {'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 + synth_tail_block_0: + type: synth_tail + tail_region_0: + type: region + kind: tail + contains: ['branch_region_4', 'branch_region_5', 'head_region_2', 'tail_region_2'] # noqa + header: head_region_2 + exiting: tail_region_2 + parent_region: meta_region_0 + tail_region_1: + type: region + kind: tail + contains: ['synth_tail_block_0'] + header: synth_tail_block_0 + exiting: synth_tail_block_0 + parent_region: branch_region_0 + tail_region_2: + type: region + kind: tail + contains: ['python_bytecode_block_5'] + header: python_bytecode_block_5 + exiting: python_bytecode_block_5 + parent_region: tail_region_0 +edges: + branch_region_0: ['tail_region_0'] + branch_region_1: ['tail_region_0'] + branch_region_2: ['tail_region_1'] + branch_region_3: ['tail_region_1'] + branch_region_4: ['tail_region_2'] + branch_region_5: ['tail_region_2'] + head_region_0: ['branch_region_0', 'branch_region_1'] + head_region_1: ['branch_region_2', 'branch_region_3'] + head_region_2: ['branch_region_4', 'branch_region_5'] + python_bytecode_block_0: ['branch_region_0', 'branch_region_1'] + python_bytecode_block_1: ['branch_region_2', 'branch_region_3'] + python_bytecode_block_2: ['synth_asign_block_1'] + python_bytecode_block_3: ['synth_asign_block_2'] + python_bytecode_block_4: ['tail_region_2'] + python_bytecode_block_5: [] + synth_asign_block_0: ['tail_region_0'] + synth_asign_block_1: ['tail_region_1'] + synth_asign_block_2: ['tail_region_1'] + synth_fill_block_0: ['tail_region_2'] + synth_head_block_0: ['branch_region_4', 'branch_region_5'] + synth_tail_block_0: ['tail_region_0'] + tail_region_0: [] + tail_region_1: ['tail_region_0'] + tail_region_2: [] +backedges:""" + + +class TestBahmannFigures(SCFGComparator): + def test_figure_3(self): + # Figure 3 of the paper + + # fake bytecode just good enough for FlowInfo + bc = [ + dis.Instruction("OP", 1, None, None, "", 0, None, False), + dis.Instruction( + "POP_JUMP_IF_TRUE", 2, None, 12, "", 2, None, False + ), + # label 4 + dis.Instruction("OP", 1, None, None, "", 4, None, False), + dis.Instruction( + "POP_JUMP_IF_TRUE", 2, None, 12, "", 6, None, False + ), + dis.Instruction("OP", 1, None, None, "", 8, None, False), + dis.Instruction("JUMP_ABSOLUTE", 2, None, 20, "", 10, None, False), + # label 12 + dis.Instruction("OP", 1, None, None, "", 12, None, False), + dis.Instruction( + "POP_JUMP_IF_TRUE", 2, None, 4, "", 14, None, False + ), + dis.Instruction("OP", 1, None, None, "", 16, None, False), + dis.Instruction("JUMP_ABSOLUTE", 2, None, 20, "", 18, None, False), + # label 20 + dis.Instruction( + "RETURN_VALUE", 1, None, None, "", 20, None, False + ), + ] + flow = FlowInfo.from_bytecode(bc) + scfg = flow.build_basicblocks() + byteflow = ByteFlow(bc=bc, scfg=scfg) + byteflow = byteflow.restructure() + + x, _ = SCFG.from_yaml(fig_3_yaml) + self.assertSCFGEqual(x, byteflow.scfg) + + def test_figure_4(self): + # Figure 4 of the paper + + # fake bytecode just good enough for FlowInfo + bc = [ + dis.Instruction("OP", 1, None, None, "", 0, None, False), + dis.Instruction( + "POP_JUMP_IF_TRUE", 2, None, 14, "", 2, None, False + ), + # label 4 + dis.Instruction("OP", 1, None, None, "", 4, None, False), + dis.Instruction( + "POP_JUMP_IF_TRUE", 2, None, 12, "", 6, None, False + ), + dis.Instruction("OP", 1, None, None, "", 8, None, False), + dis.Instruction("JUMP_ABSOLUTE", 2, None, 18, "", 10, None, False), + # label 12 + dis.Instruction("OP", 1, None, None, "", 12, None, False), + dis.Instruction("OP", 2, None, 4, "", 14, None, False), + dis.Instruction("JUMP_ABSOLUTE", 2, None, 18, "", 16, None, False), + # label 18 + dis.Instruction( + "RETURN_VALUE", 1, None, None, "", 18, None, False + ), + ] + flow = FlowInfo.from_bytecode(bc) + scfg = flow.build_basicblocks() + byteflow = ByteFlow(bc=bc, scfg=scfg) + byteflow = byteflow.restructure() + + x, _ = SCFG.from_yaml(fig_4_yaml) + self.assertSCFGEqual(x, byteflow.scfg) diff --git a/numba_rvsdg/tests/test_scfg.py b/numba_rvsdg/tests/test_scfg.py index 2bb3e3b3..cbcdcb7a 100644 --- a/numba_rvsdg/tests/test_scfg.py +++ b/numba_rvsdg/tests/test_scfg.py @@ -17,43 +17,71 @@ def test_yaml_conversion(self): # Case # 1: Acyclic graph, no back-edges cases = [ """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["4"] - "4": - jt: []""", + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['4'] + '3': ['4'] + '4': [] + backedges: + """, # Case # 2: Cyclic graph, no back edges """ - "0": - jt: ["1", "2"] - "1": - jt: ["5"] - "2": - jt: ["1", "5"] - "3": - jt: ["0"] - "4": - jt: [] - "5": - jt: ["3", "4"]""", + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['5'] + '2': ['1', '5'] + '3': ['1'] + '4': [] + '5': ['3', '4'] + backedges: + """, # Case # 3: Graph with backedges """ - "0": - jt: ["1"] - "1": - jt: ["2", "3"] - "2": - jt: ["4"] - "3": - jt: [] - "4": - jt: ["2", "3"] - be: ["2"]""", + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1'] + '1': ['2', '3'] + '2': ['4'] + '3': [] + '4': ['2', '3'] + backedges: + '4': ['2'] + """, ] for case in cases: @@ -65,28 +93,59 @@ def test_dict_conversion(self): # Case # 1: Acyclic graph, no back-edges cases = [ { - "0": {"jt": ["1", "2"]}, - "1": {"jt": ["3"]}, - "2": {"jt": ["4"]}, - "3": {"jt": ["4"]}, - "4": {"jt": []}, + "blocks": { + "0": {"type": "basic"}, + "1": {"type": "basic"}, + "2": {"type": "basic"}, + "3": {"type": "basic"}, + "4": {"type": "basic"}, + }, + "edges": { + "0": ["1", "2"], + "1": ["3"], + "2": ["4"], + "3": ["4"], + "4": [], + }, + "backedges": {}, }, # Case # 2: Cyclic graph, no back edges { - "0": {"jt": ["1", "2"]}, - "1": {"jt": ["5"]}, - "2": {"jt": ["1", "5"]}, - "3": {"jt": ["0"]}, - "4": {"jt": []}, - "5": {"jt": ["3", "4"]}, + "blocks": { + "0": {"type": "basic"}, + "1": {"type": "basic"}, + "2": {"type": "basic"}, + "3": {"type": "basic"}, + "4": {"type": "basic"}, + "5": {"type": "basic"}, + }, + "edges": { + "0": ["1", "2"], + "1": ["5"], + "2": ["1", "5"], + "3": ["0"], + "4": [], + "5": ["3", "4"], + }, + "backedges": {}, }, # Case # 3: Graph with backedges { - "0": {"jt": ["1"]}, - "1": {"jt": ["2", "3"]}, - "2": {"jt": ["4"]}, - "3": {"jt": []}, - "4": {"jt": ["2", "3"], "be": ["2"]}, + "blocks": { + "0": {"type": "basic"}, + "1": {"type": "basic"}, + "2": {"type": "basic"}, + "3": {"type": "basic"}, + "4": {"type": "basic"}, + }, + "edges": { + "0": ["1"], + "1": ["2", "3"], + "2": ["4"], + "3": [], + "4": ["2", "3"], + }, + "backedges": {"4": ["2"]}, }, ] @@ -106,10 +165,15 @@ def test_scfg_iter(self): ] scfg, _ = SCFG.from_yaml( """ - "0": - jt: ["1"] - "1": - jt: [] + blocks: + 'basic_block_0': + type: basic + 'basic_block_1': + type: basic + edges: + 'basic_block_0': ['basic_block_1'] + 'basic_block_1': [] + backedges: """ ) received = list(scfg) diff --git a/numba_rvsdg/tests/test_transforms.py b/numba_rvsdg/tests/test_transforms.py index 096b6cae..d42c2e0b 100644 --- a/numba_rvsdg/tests/test_transforms.py +++ b/numba_rvsdg/tests/test_transforms.py @@ -10,19 +10,30 @@ class TestInsertBlock(SCFGComparator): def test_linear(self): original = """ - "0": - jt: ["1"] - "1": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + edges: + '0': ['1'] + '1': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["2"] - "1": - jt: [] - "2": - jt: ["1"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['2'] + '1': [] + '2': ['1'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) new_name = original_scfg.name_gen.new_block_name(block_names.BASIC) @@ -33,23 +44,36 @@ def test_linear(self): def test_dual_predecessor(self): original = """ - "0": - jt: ["2"] - "1": - jt: ["2"] - "2": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['2'] + '1': ['2'] + '2': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["3"] - "1": - jt: ["3"] - "2": - jt: [] - "3": - jt: ["2"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['3'] + '1': ['3'] + '2': [] + '3': ['2'] + backedges: """ expected_scfg, expected_block_dict = SCFG.from_yaml(expected) new_name = original_scfg.name_gen.new_block_name(block_names.BASIC) @@ -70,23 +94,36 @@ def test_dual_predecessor(self): def test_dual_successor(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: [] - "2": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['1', '2'] + '1': [] + '2': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["3"] - "1": - jt: [] - "2": - jt: [] - "3": - jt: ["1", "2"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['3'] + '1': [] + '2': [] + '3': ['1', '2'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) original_scfg.insert_block( @@ -99,31 +136,48 @@ def test_dual_successor(self): def test_dual_predecessor_and_dual_successor(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: [] - "4": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['4'] + '3': [] + '4': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["5"] - "2": - jt: ["5"] - "3": - jt: [] - "4": - jt: [] - "5": - jt: ["3", "4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['5'] + '2': ['5'] + '3': [] + '4': [] + '5': ['3', '4'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) original_scfg.insert_block( @@ -136,31 +190,48 @@ def test_dual_predecessor_and_dual_successor(self): def test_dual_predecessor_and_dual_successor_with_additional_arcs(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["1", "4"] - "3": - jt: ["0"] - "4": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['1', '4'] + '3': ['0'] + '4': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["5"] - "2": - jt: ["1", "5"] - "3": - jt: ["0"] - "4": - jt: [] - "5": - jt: ["3", "4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['5'] + '2': ['1', '5'] + '3': ['0'] + '4': [] + '5': ['3', '4'] + backedges: """ expected_scfg, expected_block_dict = SCFG.from_yaml(expected) original_scfg.insert_block( @@ -182,23 +253,36 @@ def test_dual_predecessor_and_dual_successor_with_additional_arcs(self): class TestJoinReturns(SCFGComparator): def test_two_returns(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: [] - "2": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['1', '2'] + '1': [] + '2': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['3'] + '3': [] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) original_scfg.join_returns() @@ -208,17 +292,27 @@ def test_two_returns(self): class TestJoinTailsAndExits(SCFGComparator): def test_join_tails_and_exits_case_00(self): original = """ - "0": - jt: ["1"] - "1": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + edges: + '0': ['1'] + '1': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1"] - "1": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + edges: + '0': ['1'] + '1': [] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) @@ -234,27 +328,42 @@ def test_join_tails_and_exits_case_00(self): def test_join_tails_and_exits_case_01(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['3'] + '3': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["4"] - "1": - jt: ["3"] - "2": - jt: ["3"] - "3": - jt: [] - "4": - jt: ["1", "2"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['4'] + '1': ['3'] + '2': ['3'] + '3': [] + '4': ['1', '2'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) @@ -273,27 +382,42 @@ def test_join_tails_and_exits_case_01(self): def test_join_tails_and_exits_case_02_01(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['3'] + '3': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["4"] - "2": - jt: ["4"] - "3": - jt: [] - "4": - jt: ["3"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1', '2'] + '1': ['4'] + '2': ['4'] + '3': [] + '4': ['3'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) @@ -312,27 +436,42 @@ def test_join_tails_and_exits_case_02_01(self): def test_join_tails_and_exits_case_02_02(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["1", "3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['1', '3'] + '3': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["4"] - "2": - jt: ["1", "4"] - "3": - jt: [] - "4": - jt: ["3"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1', '2'] + '1': ['4'] + '2': ['1', '4'] + '3': [] + '4': ['3'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) @@ -351,37 +490,57 @@ def test_join_tails_and_exits_case_02_02(self): def test_join_tails_and_exits_case_03_01(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["5"] - "4": - jt: ["5"] - "5": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['4'] + '3': ['5'] + '4': ['5'] + '5': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["6"] - "2": - jt: ["6"] - "3": - jt: ["5"] - "4": - jt: ["5"] - "5": - jt: [] - "6": - jt: ["7"] - "7": - jt: ["3", "4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + edges: + '0': ['1', '2'] + '1': ['6'] + '2': ['6'] + '3': ['5'] + '4': ['5'] + '5': [] + '6': ['7'] + '7': ['3', '4'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) @@ -402,37 +561,57 @@ def test_join_tails_and_exits_case_03_01(self): def test_join_tails_and_exits_case_03_02(self): original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["1", "4"] - "3": - jt: ["5"] - "4": - jt: ["5"] - "5": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['1', '4'] + '3': ['5'] + '4': ['5'] + '5': [] + backedges: """ original_scfg, block_dict = SCFG.from_yaml(original) expected = """ - "0": - jt: ["1", "2"] - "1": - jt: ["6"] - "2": - jt: ["1", "6"] - "3": - jt: ["5"] - "4": - jt: ["5"] - "5": - jt: [] - "6": - jt: ["7"] - "7": - jt: ["3", "4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + edges: + '0': ['1', '2'] + '1': ['6'] + '2': ['1', '6'] + '3': ['5'] + '4': ['5'] + '5': [] + '6': ['7'] + '7': ['3', '4'] + backedges: """ expected_scfg, _ = SCFG.from_yaml(expected) tails = (block_dict["1"], block_dict["2"]) @@ -455,21 +634,33 @@ class TestLoopRestructure(SCFGComparator): def test_no_op_mono(self): """Loop consists of a single Block.""" original = """ - "0": - jt: ["1"] - "1": - jt: ["1", "2"] - "2": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['1'] + '1': ['1', '2'] + '2': [] + backedges: """ expected = """ - "0": - jt: ["1"] - "1": - jt: ["1", "2"] - be: ["1"] - "2": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + edges: + '0': ['1'] + '1': ['1', '2'] + '2': [] + backedges: + '1': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -479,25 +670,39 @@ def test_no_op_mono(self): def test_no_op(self): """Loop consists of two blocks, but it's in form.""" original = """ - "0": - jt: ["1"] - "1": - jt: ["2"] - "2": - jt: ["1", "3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1'] + '1': ['2'] + '2': ['1', '3'] + '3': [] + backedges: """ expected = """ - "0": - jt: ["1"] - "1": - jt: ["2"] - "2": - jt: ["1", "3"] - be: ["1"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1'] + '1': ['2'] + '2': ['1', '3'] + '3': [] + backedges: + '2': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -512,31 +717,48 @@ def test_backedge_not_exiting(self): This is the situation with the standard Python for loop. """ original = """ - "0": - jt: ["1"] - "1": - jt: ["2", "3"] - "2": - jt: ["1"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1'] + '1': ['2', '3'] + '2': ['1'] + '3': [] + backedges: """ expected = """ - "0": - jt: ["1"] - "1": - jt: ["2", "5"] - "2": - jt: ["6"] - "3": - jt: [] - "4": - jt: ["1", "3"] - be: ["1"] - "5": - jt: ["4"] - "6": - jt: ["4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + edges: + '0': ['1'] + '1': ['2', '5'] + '2': ['6'] + '3': [] + '4': ['1', '3'] + '5': ['4'] + '6': ['4'] + backedges: + '4': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -547,33 +769,51 @@ def test_backedge_not_exiting(self): def test_multi_back_edge_with_backedge_from_header(self): original = """ - "0": - jt: ["1"] - "1": - jt: ["1", "2"] - "2": - jt: ["1", "3"] - "3": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + edges: + '0': ['1'] + '1': ['1', '2'] + '2': ['1', '3'] + '3': [] + backedges: """ expected = """ - "0": - jt: ["1"] - "1": - jt: ["5", "2"] - "2": - jt: ["6", "7"] - "3": - jt: [] - "4": - jt: ["1", "3"] - be: ["1"] - "5": - jt: ["4"] - "6": - jt: ["4"] - "7": - jt: ["4"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + edges: + '0': ['1'] + '1': ['5', '2'] + '2': ['6', '7'] + '3': [] + '4': ['1', '3'] + '5': ['4'] + '6': ['4'] + '7': ['4'] + backedges: + '4': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -589,37 +829,57 @@ def test_double_exit(self): """ original = """ - "0": - jt: ["1"] - "1": - jt: ["2"] - "2": - jt: ["3", "4"] - "3": - jt: ["1", "4"] - "4": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + edges: + '0': ['1'] + '1': ['2'] + '2': ['3', '4'] + '3': ['1', '4'] + '4': [] + backedges: """ expected = """ - "0": - jt: ["1"] - "1": - jt: ["2"] - "2": - jt: ["3", "6"] - "3": - jt: ["7", "8"] - "4": - jt: [] - "5": - jt: ["1", "4"] - be: ["1"] - "6": - jt: ["5"] - "7": - jt: ["5"] - "8": - jt: ["5"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + '8': + type: basic + edges: + '0': ['1'] + '1': ['2'] + '2': ['3', '6'] + '3': ['7', '8'] + '4': [] + '5': ['1', '4'] + '6': ['5'] + '7': ['5'] + '8': ['5'] + backedges: + '5': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -633,47 +893,72 @@ def test_double_header(self): """This is like the example from Bahman2015 fig. 3 -- but with one exiting block removed.""" original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["2", "5"] - "4": - jt: ["1"] - "5": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['4'] + '3': ['2', '5'] + '4': ['1'] + '5': [] + backedges: """ expected = """ - "0": - jt: ["7", "8"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["10", "11"] - "4": - jt: ["12"] - "5": - jt: [] - "6": - jt: ["1", "2"] - "7": - jt: ["6"] - "8": - jt: ["6"] - "9": - jt: ["5", "6"] - be: ["6"] - "10": - jt: ["9"] - "11": - jt: ["9"] - "12": - jt: ["9"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + '8': + type: basic + '9': + type: basic + '10': + type: basic + '11': + type: basic + '12': + type: basic + edges: + '0': ['7', '8'] + '1': ['3'] + '2': ['4'] + '3': ['10', '11'] + '4': ['12'] + '5': [] + '6': ['1', '2'] + '7': ['6'] + '8': ['6'] + '9': ['5', '6'] + '10': ['9'] + '11': ['9'] + '12': ['9'] + backedges: + '9': ['6'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -698,59 +983,90 @@ def test_double_header_double_exiting(self): """ original = """ - "0": - jt: ["1", "2"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["2", "5"] - "4": - jt: ["1", "6"] - "5": - jt: ["7"] - "6": - jt: ["7"] - "7": - jt: [] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + edges: + '0': ['1', '2'] + '1': ['3'] + '2': ['4'] + '3': ['2', '5'] + '4': ['1', '6'] + '5': ['7'] + '6': ['7'] + '7': [] + backedges: """ expected = """ - "0": - jt: ["10", "9"] - "1": - jt: ["3"] - "2": - jt: ["4"] - "3": - jt: ["13", "14"] - "4": - jt: ["15", "16"] - "5": - jt: ["7"] - "6": - jt: ["7"] - "7": - jt: [] - "8": - jt: ["1", "2"] - "9": - jt: ["8"] - "10": - jt: ["8"] - "11": - jt: ["12", "8"] - be: ["8"] - "12": - jt: ["5", "6"] - "13": - jt: ["11"] - "14": - jt: ["11"] - "15": - jt: ["11"] - "16": - jt: ["11"] + blocks: + '0': + type: basic + '1': + type: basic + '2': + type: basic + '3': + type: basic + '4': + type: basic + '5': + type: basic + '6': + type: basic + '7': + type: basic + '8': + type: basic + '9': + type: basic + '10': + type: basic + '11': + type: basic + '12': + type: basic + '13': + type: basic + '14': + type: basic + '15': + type: basic + '16': + type: basic + edges: + '0': ['10', '9'] + '1': ['3'] + '2': ['4'] + '3': ['13', '14'] + '4': ['15', '16'] + '5': ['7'] + '6': ['7'] + '7': [] + '8': ['1', '2'] + '9': ['8'] + '10': ['8'] + '11': ['12', '8'] + '12': ['5', '6'] + '13': ['11'] + '14': ['11'] + '15': ['11'] + '16': ['11'] + backedges: + '11': ['8'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) diff --git a/numba_rvsdg/tests/test_utils.py b/numba_rvsdg/tests/test_utils.py index f1a9c4f0..488a9713 100644 --- a/numba_rvsdg/tests/test_utils.py +++ b/numba_rvsdg/tests/test_utils.py @@ -2,12 +2,17 @@ import yaml from numba_rvsdg.core.datastructures.scfg import SCFG -from numba_rvsdg.core.datastructures.basic_block import BasicBlock +from numba_rvsdg.core.datastructures.basic_block import ( + BasicBlock, + RegionBlock, + SyntheticBranch, + SyntheticAssignment, +) class SCFGComparator(TestCase): def assertSCFGEqual( - self, first_scfg: SCFG, second_scfg: SCFG, head_map=None + self, first_scfg: SCFG, second_scfg: SCFG, head_map=None, exiting=None ): if head_map: # If more than one head the corresponding map needs to be provided @@ -41,14 +46,34 @@ def assertSCFGEqual( assert len(node.jump_targets) == len(second_node.jump_targets) assert len(node.backedges) == len(second_node.backedges) + # If the given block is a RegionBlock, then the underlying SCFGs + # for both regions must be equal. + if isinstance(node, RegionBlock): + self.assertSCFGEqual( + node.subregion, second_node.subregion, exiting=node.exiting + ) + elif isinstance(node, SyntheticAssignment): + assert ( + node.variable_assignment == second_node.variable_assignment + ) + elif isinstance(node, SyntheticBranch): + assert ( + node.branch_value_table == second_node.branch_value_table + ) + assert node.variable == second_node.variable + # Add the jump targets as corresponding nodes in block mapping # dictionary. Since order must be same we can simply add zip # functionality as the correspondence function for nodes for jt1, jt2 in zip(node.jump_targets, second_node.jump_targets): + if node.name == exiting: + continue block_mapping[jt1] = jt2 stack.append(jt1) for be1, be2 in zip(node.backedges, second_node.backedges): + if node.name == exiting: + continue block_mapping[be1] = be2 stack.append(be1) @@ -75,21 +100,32 @@ def assertDictEqual( if node_name in seen: continue seen.add(node_name) - node: BasicBlock = first_yaml[node_name] # Assert that there's a corresponding mapping of current node # in second scfg assert node_name in block_mapping.keys() - # Get the corresponding node in second graph - second_node_name = block_mapping[node_name] - second_node: BasicBlock = second_yaml[second_node_name] + co_node_name = block_mapping[node_name] + + node_properties = first_yaml["blocks"][node_name] + co_node_properties = second_yaml["blocks"][co_node_name] + assert node_properties == co_node_properties + # Both nodes should have equal number of jump targets and backedges - assert len(node["jt"]) == len(second_node["jt"]) - if "be" in node.keys(): - assert len(node["be"]) == len(second_node["be"]) + assert len(first_yaml["edges"][node_name]) == len( + second_yaml["edges"][co_node_name] + ) + if first_yaml["backedges"] and first_yaml["backedges"].get( + node_name + ): + assert len(first_yaml["backedges"][node_name]) == len( + second_yaml["backedges"][co_node_name] + ) # Add the jump targets as corresponding nodes in block mapping # dictionary. Since order must be same we can simply add zip # functionality as the correspondence function for nodes - for jt1, jt2 in zip(node["jt"], second_node["jt"]): + for jt1, jt2 in zip( + first_yaml["edges"][node_name], + second_yaml["edges"][co_node_name], + ): block_mapping[jt1] = jt2 stack.append(jt1)