diff --git a/numba_rvsdg/core/datastructures/basic_block.py b/numba_rvsdg/core/datastructures/basic_block.py index 263e09cc..b1489476 100644 --- a/numba_rvsdg/core/datastructures/basic_block.py +++ b/numba_rvsdg/core/datastructures/basic_block.py @@ -1,8 +1,9 @@ import dis from typing import Tuple, Dict, List -from dataclasses import dataclass, replace +from dataclasses import dataclass, replace, field from numba_rvsdg.core.utils import _next_inst_offset +from numba_rvsdg.core.datastructures import block_names @dataclass(frozen=True) @@ -19,15 +20,20 @@ class BasicBlock: _jump_targets: Tuple[str] Jump targets (branch destinations) for this block. - backedges: Tuple[str] - Backedges for this block. + backedges: Tuple[bool] + Indicates if the Jump target at the particular index + is a backedge or not. """ name: str _jump_targets: Tuple[str] = tuple() - backedges: Tuple[str] = tuple() + backedges: Tuple[bool] = field(init=False) + + def __post_init__(self): + backedges = tuple([False] * len(self._jump_targets)) + object.__setattr__(self, "backedges", backedges) @property def is_exiting(self) -> bool: @@ -69,32 +75,27 @@ def jump_targets(self) -> Tuple[str]: """ acc = [] - for j in self._jump_targets: - if j not in self.backedges: + for idx, j in enumerate(self._jump_targets): + if not self.backedges[idx]: acc.append(j) return tuple(acc) - def declare_backedge(self, target: str) -> "BasicBlock": + def declare_backedge(self, target: str): """Declare one of the jump targets as a backedge of this block. Parameters ---------- target: str The jump target that is to be declared as a backedge. - - Returns - ------- - basic_block: BasicBlock - The resulting block. - """ - if target in self.jump_targets: - assert not self.backedges - return replace(self, backedges=(target,)) - return self + assert target in self._jump_targets + idx = self._jump_targets.index(target) + current_backedges = list(self.backedges) + current_backedges[idx] = True + object.__setattr__(self, "backedges", tuple(current_backedges)) - def replace_jump_targets(self, jump_targets: Tuple) -> "BasicBlock": - """Replaces jump targets of this block by the given tuple. + def change_jump_targets(self, jump_targets: Tuple): + """Changes jump targets of this block by the given tuple. This method replaces the jump targets of the current BasicBlock. The provided jump targets must be in the same order as their @@ -108,34 +109,18 @@ def replace_jump_targets(self, jump_targets: Tuple) -> "BasicBlock": ---------- jump_targets: Tuple The new jump target tuple. Must be ordered. - - Returns - ------- - basic_block: BasicBlock - The resulting BasicBlock. - """ - return replace(self, _jump_targets=jump_targets) + is_backedge = {} + new_backedges = [] - def replace_backedges(self, backedges: Tuple) -> "BasicBlock": - """Replaces back edges of this block by the given tuple. + for idx, i in enumerate(self.backedges): + is_backedge[self._jump_targets[idx]] = i - This method replaces the back edges of the current BasicBlock. - The provided back edges must be in the same order as their - intended original replacements. + for i in jump_targets: + new_backedges.append(is_backedge.get(i, False)) - Parameters - ---------- - backedges: Tuple - The new back edges tuple. Must be ordered. - - Returns - ------- - basic_block: BasicBlock - The resulting BasicBlock. - - """ - return replace(self, backedges=backedges) + object.__setattr__(self, "_jump_targets", jump_targets) + object.__setattr__(self, "backedges", new_backedges) @dataclass(frozen=True) @@ -156,6 +141,8 @@ class PythonBytecodeBlock(BasicBlock): end: int = None + bcmap: dis.Bytecode = None + def get_instructions( self, bcmap: Dict[int, dis.Instruction] ) -> List[dis.Instruction]: @@ -233,8 +220,8 @@ class SyntheticFill(SyntheticBlock): @dataclass(frozen=True) class SyntheticAssignment(SyntheticBlock): - """The SyntheticAssignment class represents a artificially added assignment block - in a structured control flow graph (SCFG). + """The SyntheticAssignment class represents a artificially added + assignment block in a structured control flow graph (SCFG). This block is responsible for giving variables their values, once the respective block is executed. @@ -384,3 +371,30 @@ def replace_exiting(self, new_exiting): The new exiting block of the region represented by the RegionBlock. """ object.__setattr__(self, "exiting", new_exiting) + + def replace_parent(self, new_parent): + """This method performs a inplace replacement of the parent region + block. + + Parameters + ---------- + new_exiting: str + The new exiting block of the region represented by the RegionBlock. + """ + object.__setattr__(self, "parent", new_parent) + + +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/byte_flow.py b/numba_rvsdg/core/datastructures/byte_flow.py deleted file mode 100644 index 9b1ebb04..00000000 --- a/numba_rvsdg/core/datastructures/byte_flow.py +++ /dev/null @@ -1,153 +0,0 @@ -import dis -from copy import deepcopy -from dataclasses import dataclass - -from numba_rvsdg.core.datastructures.scfg import SCFG -from numba_rvsdg.core.datastructures.basic_block import RegionBlock -from numba_rvsdg.core.datastructures.flow_info import FlowInfo -from numba_rvsdg.core.utils import _logger, _LogWrap - -from numba_rvsdg.core.transformations import ( - restructure_loop, - restructure_branch, -) - - -@dataclass(frozen=True) -class ByteFlow: - """ByteFlow class. - - The ByteFlow class represents the bytecode and its relation with - corresponding structured control flow graph (SCFG). - - Attributes - ---------- - bc: dis.Bytecode - The dis.Bytecode object representing the bytecode. - scfg: SCFG - The SCFG object representing the control flow of - the bytecode. - """ - - bc: dis.Bytecode - scfg: "SCFG" - - @staticmethod - def from_bytecode(code) -> "ByteFlow": - """Creates a ByteFlow object from the given python - function. - - This method uses dis.Bytecode to parse the bytecode - generated from the given Python function. - It returns a ByteFlow object with the corresponding - bytecode and SCFG. - - Parameters - ---------- - code: Python Function - The Python Function from which ByteFlow is to - be generated. - - Returns - ------- - byteflow: ByteFlow - The resulting ByteFlow object. - """ - bc = dis.Bytecode(code) - _logger.debug("Bytecode\n%s", _LogWrap(lambda: bc.dis())) - - flowinfo = FlowInfo.from_bytecode(bc) - scfg = flowinfo.build_basicblocks() - return ByteFlow(bc=bc, scfg=scfg) - - def _join_returns(self): - """Joins the return blocks within the corresponding SCFG. - - This method creates a deep copy of the SCFG and performs - operation to join return blocks within the control flow. - It returns a new ByteFlow object with the updated SCFG. - - Returns - ------- - byteflow: ByteFlow - The new ByteFlow object with updated SCFG. - """ - scfg = deepcopy(self.scfg) - scfg.join_returns() - return ByteFlow(bc=self.bc, scfg=scfg) - - def _restructure_loop(self): - """Restructures the loops within the corresponding SCFG. - - Creates a deep copy of the SCFG and performs the operation to - restructure loop constructs within the control flow using - the algorithm LOOP RESTRUCTURING from section 4.1 of Bahmann2015. - It applies the restructuring operation to both the main SCFG - and any subregions within it. It returns a new ByteFlow object - with the updated SCFG. - - Returns - ------- - byteflow: ByteFlow - The new ByteFlow object with updated SCFG. - """ - scfg = deepcopy(self.scfg) - restructure_loop(scfg.region) - for region in _iter_subregions(scfg): - restructure_loop(region) - return ByteFlow(bc=self.bc, scfg=scfg) - - def _restructure_branch(self): - """Restructures the branches within the corresponding SCFG. - - Creates a deep copy of the SCFG and performs the operation to - restructure branch constructs within the control flow. It applies - the restructuring operation to both the main SCFG and any - subregions within it. It returns a new ByteFlow object with - the updated SCFG. - - Returns - ------- - byteflow: ByteFlow - The new ByteFlow object with updated SCFG. - """ - scfg = deepcopy(self.scfg) - restructure_branch(scfg.region) - for region in _iter_subregions(scfg): - restructure_branch(region) - return ByteFlow(bc=self.bc, scfg=scfg) - - def restructure(self): - """Applies join_returns, restructure_loop and restructure_branch - in the respective order on the SCFG. - - Creates a deep copy of the SCFG and applies a series of - restructuring operations to it. The operations include - joining return blocks, restructuring loop constructs, and - restructuring branch constructs. It returns a new ByteFlow - object with the updated SCFG. - - Returns - ------- - byteflow: ByteFlow - The new ByteFlow object with updated SCFG. - """ - scfg = deepcopy(self.scfg) - # close - scfg.join_returns() - # handle loop - restructure_loop(scfg.region) - for region in _iter_subregions(scfg): - restructure_loop(region) - # handle branch - restructure_branch(scfg.region) - for region in _iter_subregions(scfg): - restructure_branch(region) - return ByteFlow(bc=self.bc, scfg=scfg) - - -def _iter_subregions(scfg: "SCFG"): - for node in scfg.graph.values(): - if isinstance(node, RegionBlock): - yield node - yield from _iter_subregions(node.subregion) diff --git a/numba_rvsdg/core/datastructures/flow_info.py b/numba_rvsdg/core/datastructures/flow_info.py deleted file mode 100644 index 562bf864..00000000 --- a/numba_rvsdg/core/datastructures/flow_info.py +++ /dev/null @@ -1,142 +0,0 @@ -import dis - -from typing import Set, Tuple, Dict, Sequence -from dataclasses import dataclass, field - -from numba_rvsdg.core.datastructures.basic_block import PythonBytecodeBlock -from numba_rvsdg.core.datastructures import block_names -from numba_rvsdg.core.datastructures.scfg import SCFG -from numba_rvsdg.core.utils import ( - is_conditional_jump, - _next_inst_offset, - is_unconditional_jump, - is_exiting, - _prev_inst_offset, -) - - -@dataclass() -class FlowInfo: - """The FlowInfo class is responsible for converting bytecode into a - ByteFlow object. - - Attributes - ---------- - block_offsets: Set[int] - A set that marks the starting offsets of basic blocks in the bytecode. - jump_insts: Dict[int, Tuple[int, ...]] - A dictionary that contains jump instructions and their target offsets. - last_offset: int - The offset of the last bytecode instruction. - """ - - block_offsets: Set[int] = field(default_factory=set) - - jump_insts: Dict[int, Tuple[int, ...]] = field(default_factory=dict) - - last_offset: int = field(default=0) - - def _add_jump_inst(self, offset: int, targets: Sequence[int]): - """Internal method to add a jump instruction to the FlowInfo. - - This method adds the target offsets of the jump instruction - to the block_offsets set and updates the jump_insts dictionary. - - Parameters - ---------- - offset: int - The given target offset. - targets: Sequence[int] - target jump instrcutions. - """ - for off in targets: - assert isinstance(off, int) - self.block_offsets.add(off) - self.jump_insts[offset] = tuple(targets) - - @staticmethod - def from_bytecode(bc: dis.Bytecode) -> "FlowInfo": - """Static method that builds the structured control flow graph (SCFG) - from the given `dis.Bytecode` object bc. - - This method analyzes the bytecode instructions, marks the start of - basic blocks, and records jump instructions and their target offsets. - It builds the structured control flow graph (SCFG) from the given - `dis.Bytecode` object and returns a FlowInfo object. - - Parameters - ---------- - bc: dis.Bytecode - Bytecode from which flowinfo is to be constructed. - - Returns - ------- - flowinfo: FlowInfo - FlowInfo object representing the given bytecode. - """ - flowinfo = FlowInfo() - - for inst in bc: - # Handle jump-target instruction - if inst.offset == 0 or inst.is_jump_target: - flowinfo.block_offsets.add(inst.offset) - # Handle by op - if is_conditional_jump(inst.opname): - flowinfo._add_jump_inst( - inst.offset, (_next_inst_offset(inst.offset), inst.argval) - ) - elif is_unconditional_jump(inst.opname): - flowinfo._add_jump_inst(inst.offset, (inst.argval,)) - elif is_exiting(inst.opname): - flowinfo._add_jump_inst(inst.offset, ()) - - flowinfo.last_offset = inst.offset - return flowinfo - - def build_basicblocks(self: "FlowInfo", end_offset=None) -> "SCFG": - """Builds a graph of basic blocks based on the flow information. - - It creates a structured control flow graph (SCFG) object, assigns - names to the blocks, and defines the block boundaries, jump targets, - and backedges. It returns an SCFG object representing the control - flow graph. - - Parameters - ---------- - end_offset: int - The byte offset of the last instruction. - - Returns - ------- - scfg: SCFG - SCFG object corresponding to the bytecode contained within the - current FlowInfo object. - """ - scfg = SCFG() - offsets = sorted(self.block_offsets) - # enumerate names - names = { - offset: scfg.name_gen.new_block_name(block_names.PYTHON_BYTECODE) - for offset in offsets - } - if end_offset is None: - end_offset = _next_inst_offset(self.last_offset) - - for begin, end in zip(offsets, [*offsets[1:], end_offset]): - name = names[begin] - targets: Tuple[str, ...] - term_offset = _prev_inst_offset(end) - if term_offset not in self.jump_insts: - # implicit jump - targets = (names[end],) - else: - targets = tuple(names[o] for o in self.jump_insts[term_offset]) - block = PythonBytecodeBlock( - name=name, - begin=begin, - end=end, - _jump_targets=targets, - backedges=(), - ) - scfg.add_block(block) - return scfg diff --git a/numba_rvsdg/core/datastructures/scfg.py b/numba_rvsdg/core/datastructures/scfg.py index 16f96078..4b894fb1 100644 --- a/numba_rvsdg/core/datastructures/scfg.py +++ b/numba_rvsdg/core/datastructures/scfg.py @@ -1,10 +1,11 @@ import dis import yaml -from textwrap import dedent -from typing import Set, Tuple, Dict, List, Iterator +from textwrap import indent +from typing import Set, Tuple, Dict, List, Iterator, Sequence from dataclasses import dataclass, field from collections import deque from collections.abc import Mapping +from numba_rvsdg.core.utils import _logger, _LogWrap from numba_rvsdg.core.datastructures.basic_block import ( BasicBlock, @@ -15,9 +16,30 @@ SyntheticTail, SyntheticReturn, SyntheticFill, + PythonBytecodeBlock, RegionBlock, + SyntheticBranch, + block_type_names, +) +from numba_rvsdg.core.datastructures.block_names import ( + block_types, + PYTHON_BYTECODE, + SYNTH_HEAD, + SYNTH_BRANCH, + SYNTH_TAIL, + SYNTH_EXIT, + SYNTH_ASSIGN, + SYNTH_RETURN, + SYNTH_EXIT_LATCH, + SYNTH_EXIT_BRANCH, +) +from numba_rvsdg.core.utils import ( + is_conditional_jump, + _next_inst_offset, + is_unconditional_jump, + is_exiting, + _prev_inst_offset, ) -from numba_rvsdg.core.datastructures import block_names @dataclass(frozen=True) @@ -496,15 +518,13 @@ def insert_block( """ # TODO: needs a diagram and documentaion # initialize new block - new_block = block_type( - name=new_name, _jump_targets=successors, backedges=set() - ) + new_block = block_type(name=new_name, _jump_targets=successors) # add block to self self.add_block(new_block) # Replace any arcs from any of predecessors to any of successors with # an arc through the inserted block instead. for name in predecessors: - block = self.graph.pop(name) + block = self.graph[name] jt = list(block.jump_targets) if successors: for s in successors: @@ -515,7 +535,7 @@ def insert_block( jt.pop(jt.index(s)) else: jt.append(new_name) - self.add_block(block.replace_jump_targets(jump_targets=tuple(jt))) + block.change_jump_targets(jump_targets=tuple(jt)) def insert_SyntheticExit( self, @@ -604,15 +624,12 @@ def insert_block_and_control_blocks( # 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 - ) + synth_assign = self.name_gen.new_block_name(SYNTH_ASSIGN) variable_assignment = {} variable_assignment[branch_variable] = branch_variable_value synth_assign_block = SyntheticAssignment( name=synth_assign, _jump_targets=(new_name,), - backedges=(), variable_assignment=variable_assignment, ) # add block @@ -624,16 +641,11 @@ def insert_block_and_control_blocks( # replace previous successor with synth_assign jt[jt.index(s)] = synth_assign # finally, replace the jump_targets - self.add_block( - self.graph.pop(name).replace_jump_targets( - jump_targets=tuple(jt) - ) - ) + self.graph[name].change_jump_targets(jump_targets=tuple(jt)) # initialize new block, which will hold the branching table new_block = SyntheticHead( name=new_name, _jump_targets=tuple(successors), - backedges=set(), variable=branch_variable, branch_value_table=branch_value_table, ) @@ -652,9 +664,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 +695,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 @@ -731,7 +733,7 @@ def bcmap_from_bytecode(bc: dis.Bytecode): return {inst.offset: inst for inst in bc} @staticmethod - def from_yaml(yaml_string): + def from_yaml(yaml_string: str): """Static method that creates an SCFG object from a YAML representation. @@ -781,23 +783,107 @@ def from_dict(graph_dict: dict): Dictionary of block names in YAML string corresponding to their representation/unique name IDs in the SCFG. """ - scfg_graph = {} 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_graph[name] = block - scfg = SCFG(scfg_graph, name_gen=name_gen) - return scfg, block_dict + block_ref_dict = {} + + blocks = graph_dict["blocks"] + edges = graph_dict["edges"] + backedges = graph_dict["backedges"] + if backedges is None: + backedges = {} + + for key, block in blocks.items(): + assert block["type"] in block_types + block_ref_dict[key] = key + + # Find head of the graph, i.e. node which isn't in anyones contains + # and no edges point towards it (backedges are allowed) + heads = set(blocks.keys()) + for block_name, block_data in blocks.items(): + if block_data.get("contains"): + heads.difference_update(block_data["contains"]) + jump_targets = set(edges[block_name]) + if backedges.get(block_name): + jump_targets.difference_update(set(backedges[block_name])) + heads.difference_update(jump_targets) + assert len(heads) > 0 + + seen = set() + + def make_scfg(curr_heads: set, exiting: str = None): + scfg_graph = {} + queue = curr_heads + while queue: + current_name = queue.pop() + if current_name in seen: + continue + seen.add(current_name) + + block_info = blocks[current_name] + block_edges = tuple( + block_ref_dict[idx] for idx in edges[current_name] + ) + if backedges and backedges.get(current_name): + block_backedges = tuple( + block_ref_dict[idx] for idx in backedges[current_name] + ) + else: + block_backedges = () + block_type = block_info.get("type") + block_class = block_type_names[block_type] + if block_type == "region": + scfg = make_scfg( + {block_info["header"]}, block_info["exiting"] + ) + block = RegionBlock( + name=current_name, + _jump_targets=block_edges, + kind=block_info["kind"], + header=block_info["header"], + exiting=block_info["exiting"], + subregion=scfg, + ) + elif block_type in [ + SYNTH_BRANCH, + SYNTH_HEAD, + SYNTH_EXIT_LATCH, + SYNTH_EXIT_BRANCH, + ]: + block = block_class( + name=current_name, + _jump_targets=block_edges, + branch_value_table=block_info["branch_value_table"], + variable=block_info["variable"], + ) + elif block_type in [SYNTH_ASSIGN]: + block = SyntheticAssignment( + name=current_name, + _jump_targets=block_edges, + variable_assignment=block_info["variable_assignment"], + ) + elif block_type in [PYTHON_BYTECODE]: + block = PythonBytecodeBlock( + name=current_name, + _jump_targets=block_edges, + begin=block_info["begin"], + end=block_info["end"], + ) + else: + block = block_class( + name=current_name, + _jump_targets=block_edges, + ) + for backedge in block_backedges: + block.declare_backedge(backedge) + scfg_graph[current_name] = block + if current_name != exiting: + queue.update(edges[current_name]) + + scfg = SCFG(scfg_graph, name_gen=name_gen) + return scfg + + scfg = make_scfg(heads) + return scfg, block_ref_dict def to_yaml(self): """Converts the SCFG object to a YAML string representation. @@ -813,24 +899,29 @@ def to_yaml(self): A YAML string representing the SCFG. """ # Convert to yaml - scfg_graph = self.graph - yaml_string = """""" + ys = "" + + graph_dict = self.to_dict() - 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}""" + blocks = graph_dict["blocks"] + edges = graph_dict["edges"] + backedges = graph_dict["backedges"] - if back_edges: - back_edges = str(back_edges).replace("'", '"') - jump_target_str += f""" - be: {back_edges}""" - yaml_string += dedent(jump_target_str) + 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) - return yaml_string + 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 def to_dict(self): """Converts the SCFG object to a dictionary representation. @@ -845,14 +936,45 @@ def to_dict(self): 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.") + + for key, value in self: + block_type = reverse_lookup(type(value)) + blocks[key] = {"type": block_type} + if isinstance(value, RegionBlock): + 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 idx, i in enumerate(value._jump_targets) + if value.backedges[idx] + ] + ) + + graph_dict = {"blocks": blocks, "edges": edges, "backedges": backedges} + return graph_dict def view(self, name: str = None): @@ -871,6 +993,124 @@ def view(self, name: str = None): SCFGRenderer(self).view(name) + def restructure_loop(self): + """Restructures the loops within the corresponding SCFG using + the algorithm LOOP RESTRUCTURING from section 4.1 of Bahmann2015. + It applies the restructuring operation to both the main SCFG + and any subregions within it. + """ + from numba_rvsdg.core.transformations import restructure_loop + + restructure_loop(self.region) + for region in _iter_subregions(self): + restructure_loop(region) + + def restructure_branch(self): + """Restructures the branches within the corresponding SCFG. It applies + the restructuring operation to both the main SCFG and any + subregions within it. + """ + from numba_rvsdg.core.transformations import restructure_branch + + restructure_branch(self.region) + for region in _iter_subregions(self): + restructure_branch(region) + + def restructure(self): + """Applies join_returns, restructure_loop and restructure_branch + in the respective order on the SCFG. Applies a series of + restructuring operations to it. The operations include + joining return blocks, restructuring loop constructs, and + restructuring branch constructs. + """ + # close + self.join_returns() + # handle loop + self.restructure_loop() + # handle branch + self.restructure_branch() + + @staticmethod + def from_bytecode(function): + bc = dis.Bytecode(function) + return SCFG._from_bytecode(bc) + + @staticmethod + def _from_bytecode(code: list, end_offset=None): + _logger.debug("Bytecode\n%s", _LogWrap(lambda: code.dis())) + block_offsets: Set[int] = set() + jump_insts: Dict[int, Tuple[int, ...]] = {} + last_offset: int = 0 + + def _add_jump_inst(offset: int, targets: Sequence[int]): + """Internal method to add a jump instruction. + + This method adds the target offsets of the jump instruction + to the block_offsets set and updates the jump_insts dictionary. + + Parameters + ---------- + offset: int + The given target offset. + targets: Sequence[int] + target jump instrcutions. + """ + for off in targets: + assert isinstance(off, int) + block_offsets.add(off) + jump_insts[offset] = tuple(targets) + + for inst in code: + # Handle jump-target instruction + if inst.offset == 0 or inst.is_jump_target: + block_offsets.add(inst.offset) + # Handle by op + if is_conditional_jump(inst.opname): + _add_jump_inst( + inst.offset, (_next_inst_offset(inst.offset), inst.argval) + ) + elif is_unconditional_jump(inst.opname): + _add_jump_inst(inst.offset, (inst.argval,)) + elif is_exiting(inst.opname): + _add_jump_inst(inst.offset, ()) + + last_offset = inst.offset + + scfg = SCFG() + offsets = sorted(block_offsets) + # enumerate names + names = { + offset: scfg.name_gen.new_block_name(PYTHON_BYTECODE) + for offset in offsets + } + if end_offset is None: + end_offset = _next_inst_offset(last_offset) + + for begin, end in zip(offsets, [*offsets[1:], end_offset]): + name = names[begin] + targets: Tuple[str, ...] + term_offset = _prev_inst_offset(end) + if term_offset not in jump_insts: + # implicit jump + targets = (names[end],) + else: + targets = tuple(names[o] for o in jump_insts[term_offset]) + block = PythonBytecodeBlock( + name=name, + begin=begin, + end=end, + _jump_targets=targets, + ) + scfg.add_block(block) + return scfg + + +def _iter_subregions(scfg: "SCFG"): + for node in scfg.graph.values(): + if isinstance(node, RegionBlock): + yield node + yield from _iter_subregions(node.subregion) + class AbstractGraphView(Mapping): """Abstract Graph View class. diff --git a/numba_rvsdg/core/transformations.py b/numba_rvsdg/core/transformations.py index 812024de..e6c7e0f9 100644 --- a/numba_rvsdg/core/transformations.py +++ b/numba_rvsdg/core/transformations.py @@ -58,9 +58,7 @@ def loop_restructure_helper(scfg: SCFG, loop: Set[str]): and len(exiting_blocks) == 1 and backedge_blocks[0] == next(iter(exiting_blocks)) ): - scfg.add_block( - scfg.graph.pop(backedge_blocks[0]).declare_backedge(loop_head) - ) + scfg.graph[backedge_blocks[0]].declare_backedge(loop_head) return # The synthetic exiting latch and synthetic exit need to be created @@ -147,7 +145,6 @@ def reverse_lookup(d, value): synth_assign_block = SyntheticAssignment( name=synth_assign, _jump_targets=(synth_exiting_latch,), - backedges=(), variable_assignment=variable_assignment, ) # Insert the assignment to the scfg @@ -177,20 +174,17 @@ def reverse_lookup(d, value): # that point to the headers, no need to add a backedge, # since it will be contained in the SyntheticExitingLatch # later on. - block = scfg.graph.pop(name) + block = scfg.graph[name] jts = list(block.jump_targets) for h in headers: if h in jts: jts.remove(h) - scfg.add_block( - block.replace_jump_targets(jump_targets=tuple(jts)) - ) + block.change_jump_targets(jump_targets=tuple(jts)) # Setup the assignment block and initialize it with the # correct jump_targets and variable assignment. synth_assign_block = SyntheticAssignment( name=synth_assign, _jump_targets=(synth_exiting_latch,), - backedges=(), variable_assignment=variable_assignment, ) # Add the new block to the SCFG @@ -199,11 +193,7 @@ def reverse_lookup(d, value): new_jt[new_jt.index(jt)] = synth_assign # finally, replace the jump_targets for this block with the new # ones - scfg.add_block( - scfg.graph.pop(name).replace_jump_targets( - jump_targets=tuple(new_jt) - ) - ) + scfg.graph[name].change_jump_targets(jump_targets=tuple(new_jt)) # Add any new blocks to the loop. loop.update(new_blocks) @@ -214,10 +204,10 @@ def reverse_lookup(d, value): synth_exit if needs_synth_exit else next(iter(exit_blocks)), loop_head, ), - backedges=(loop_head,), variable=backedge_variable, branch_value_table=backedge_value_table, ) + synth_exiting_latch_block.declare_backedge(loop_head) loop.add(synth_exiting_latch) scfg.add_block(synth_exiting_latch_block) # If an exit is to be created, we do so too, but only add it to the scfg, @@ -226,7 +216,6 @@ def reverse_lookup(d, value): synth_exit_block = SyntheticExitBranch( name=synth_exit, _jump_targets=tuple(exit_blocks), - backedges=(), variable=exit_variable, branch_value_table=exit_value_table, ) @@ -329,29 +318,18 @@ def update_exiting( ): # Recursively updates the exiting blocks of a regionblock region_exiting = region_block.exiting - region_exiting_block: BasicBlock = region_block.subregion.graph.pop( + region_exiting_block: BasicBlock = region_block.subregion.graph[ region_exiting - ) + ] jt = list(region_exiting_block._jump_targets) for idx, s in enumerate(jt): if s is new_region_header: jt[idx] = new_region_name - region_exiting_block = region_exiting_block.replace_jump_targets( - jump_targets=tuple(jt) - ) - be = list(region_exiting_block.backedges) - for idx, s in enumerate(be): - if s is new_region_header: - be[idx] = new_region_name - region_exiting_block = region_exiting_block.replace_backedges( - backedges=tuple(be) - ) + region_exiting_block.change_jump_targets(jump_targets=tuple(jt)) if isinstance(region_exiting_block, RegionBlock): region_exiting_block = update_exiting( region_exiting_block, new_region_header, new_region_name ) - region_block.subregion.add_block(region_exiting_block) - return region_block def extract_region( @@ -381,27 +359,20 @@ def extract_region( # the SCFG represents should not be the meta region. assert scfg.region.kind != "meta" continue - entry = scfg.graph.pop(name) + entry = scfg.graph[name] jt = list(entry._jump_targets) for idx, s in enumerate(jt): if s is region_header: jt[idx] = region_name - entry = entry.replace_jump_targets(jump_targets=tuple(jt)) - be = list(entry.backedges) - for idx, s in enumerate(be): - if s is region_header: - be[idx] = region_name - entry = entry.replace_backedges(backedges=tuple(be)) + entry.change_jump_targets(jump_targets=tuple(jt)) # If the entry itself is a region, update it's # exiting blocks too, recursively if isinstance(entry, RegionBlock): - entry = update_exiting(entry, region_header, region_name) - scfg.add_block(entry) + update_exiting(entry, region_header, region_name) region = RegionBlock( name=region_name, _jump_targets=scfg[region_exiting].jump_targets, - backedges=(), kind=region_kind, header=region_header, subregion=head_subgraph, @@ -426,7 +397,7 @@ def extract_region( # update the parent region for k, v in region.subregion.graph.items(): if isinstance(v, RegionBlock): - object.__setattr__(v, "parent_region", region) + v.replace_parent(region) def restructure_branch(parent_region: RegionBlock): diff --git a/numba_rvsdg/rendering/rendering.py b/numba_rvsdg/rendering/rendering.py index d0671c8b..a6c14318 100644 --- a/numba_rvsdg/rendering/rendering.py +++ b/numba_rvsdg/rendering/rendering.py @@ -8,19 +8,29 @@ SyntheticBlock, ) from numba_rvsdg.core.datastructures.scfg import SCFG -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow -import dis -from typing import Dict -class BaseRenderer: - """Base Renderer class. +class SCFGRenderer: + """The `SCFGRenderer` class is used to render the visual + representation of a `SCFG` object. + + Attributes + ---------- + g: Digraph + The graphviz Digraph object that represents the entire graph upon + which the current SCFG is to be rendered. - This is the base class for all types of graph renderers. It defines two - methods `render_block` and `render_edges` that define how the blocks and - edges of the graph are rendered respectively. """ + def __init__(self, scfg: SCFG): + from graphviz import Digraph + + self.g = Digraph() + # render nodes + for name, block in scfg.graph.items(): + self.render_block(self.g, name, block) + self.render_edges(scfg) + def render_block( self, digraph: "Digraph", name: str, block: BasicBlock # noqa ): @@ -74,135 +84,22 @@ def find_base_header(block: BasicBlock): if isinstance(src_block, RegionBlock): continue src_block = find_base_header(src_block) - for dst_name in src_block.jump_targets: - dst_name = find_base_header(blocks[dst_name]).name - if dst_name in blocks.keys(): - self.g.edge(str(src_block.name), str(dst_name)) - else: - raise Exception("unreachable " + str(src_block)) - for dst_name in src_block.backedges: + for idx, dst_name in enumerate(src_block._jump_targets): dst_name = find_base_header(blocks[dst_name]).name if dst_name in blocks.keys(): - self.g.edge( - str(src_block.name), - str(dst_name), - style="dashed", - color="grey", - constraint="0", - ) + if src_block.backedges[idx]: + self.g.edge( + str(src_block.name), + str(dst_name), + style="dashed", + color="grey", + constraint="0", + ) + else: + self.g.edge(str(src_block.name), str(dst_name)) else: raise Exception("unreachable " + str(src_block)) - -class ByteFlowRenderer(BaseRenderer): - """The `ByteFlowRenderer` class is used to render the visual - representation of a `ByteFlow` object. - - Attributes - ---------- - g: Digraph - The graphviz Digraph object that represents the entire graph upon - which the current ByteFlow is to be rendered. - bcmap: Dict[int, dis.Instruction] - Mapping of bytecode offset to instruction. - - """ - - def __init__(self): - from graphviz import Digraph - - self.g = Digraph() - - def render_region_block( - self, digraph: "Digraph", name: str, regionblock: RegionBlock # noqa - ): - # render subgraph - with digraph.subgraph(name=f"cluster_{name}") as subg: - color = "blue" - if regionblock.kind == "branch": - color = "green" - if regionblock.kind == "tail": - color = "purple" - if regionblock.kind == "head": - color = "red" - subg.attr(color=color, label=regionblock.name) - for name, block in regionblock.subregion.graph.items(): - self.render_block(subg, name, block) - - def render_basic_block( - self, digraph: "Digraph", name: str, block: BasicBlock # noqa - ): - if name.startswith("python_bytecode"): - instlist = block.get_instructions(self.bcmap) - body = name + r"\l" - body += r"\l".join( - [f"{inst.offset:3}: {inst.opname}" for inst in instlist] + [""] - ) - else: - body = name + r"\l" - - digraph.node(str(name), shape="rect", label=body) - - def render_control_variable_block( - self, digraph: "Digraph", name: str, block: BasicBlock # noqa - ): - if isinstance(name, str): - body = name + r"\l" - body += r"\l".join( - (f"{k} = {v}" for k, v in block.variable_assignment.items()) - ) - else: - raise Exception("Unknown name type: " + name) - digraph.node(str(name), shape="rect", label=body) - - def render_branching_block( - self, digraph: "Digraph", name: str, block: BasicBlock # noqa - ): - if isinstance(name, str): - body = name + r"\l" - body += rf"variable: {block.variable}\l" - body += r"\l".join( - (f"{k}=>{v}" for k, v in block.branch_value_table.items()) - ) - else: - raise Exception("Unknown name type: " + name) - digraph.node(str(name), shape="rect", label=body) - - def render_byteflow(self, byteflow: ByteFlow): - """Renders the provided `ByteFlow` object. - """ - self.bcmap_from_bytecode(byteflow.bc) - # render nodes - for name, block in byteflow.scfg.graph.items(): - self.render_block(self.g, name, block) - self.render_edges(byteflow.scfg) - return self.g - - def bcmap_from_bytecode(self, bc: dis.Bytecode): - self.bcmap: Dict[int, dis.Instruction] = SCFG.bcmap_from_bytecode(bc) - - -class SCFGRenderer(BaseRenderer): - """The `SCFGRenderer` class is used to render the visual - representation of a `SCFG` object. - - Attributes - ---------- - g: Digraph - The graphviz Digraph object that represents the entire graph upon - which the current SCFG is to be rendered. - - """ - - def __init__(self, scfg: SCFG): - from graphviz import Digraph - - self.g = Digraph() - # render nodes - for name, block in scfg.graph.items(): - self.render_block(self.g, name, block) - self.render_edges(scfg) - def render_region_block( self, digraph: "Digraph", name: str, regionblock: RegionBlock # noqa ): @@ -297,7 +194,7 @@ def view(self, name: str): def render_func(func): """The `render_func`` function takes a `func` parameter as the Python - function to be transformed and rendered and renders the byte flow + function to be transformed and rendered and renders the SCFG representation of the bytecode of the function. Parameters @@ -305,53 +202,40 @@ def render_func(func): func: Python function The Python function for which bytecode is to be rendered. """ - render_flow(ByteFlow.from_bytecode(func)) + render_scfg(SCFG.from_bytecode(func)) -def render_flow(flow): - """Renders multiple ByteFlow representations across various SCFG +def render_scfg(scfg): + """Renders multiple SCFG representations across various transformations. - The `render_flow`` function takes a `flow` parameter as the `ByteFlow` + The `render_scfg`` function takes a `scfg` parameter as the `SCFG` to be transformed and rendered and performs the following operations: - - Renders the pure `ByteFlow` representation of the function using - `ByteFlowRenderer` and displays it as a document named "before". + - Renders the pure `SCFG` representation of the function using + `SCFGRenderer` and displays it as a document named "before". - - Joins the return blocks in the `ByteFlow` object graph and renders + - Joins the return blocks in the `SCFG` object graph and renders the graph, displaying it as a document named "closed". - - Restructures the loops recursively in the `ByteFlow` object graph + - Restructures the loops recursively in the `SCFG` object graph and renders the graph, displaying it as named "loop restructured". - - Restructures the branch recursively in the `ByteFlow` object graph + - Restructures the branch recursively in the `SCFG` object graph and renders the graph, displaying it as named "branch restructured". Parameters ---------- - flow: ByteFlow - The ByteFlow object to be trnasformed and rendered. + scfg: SCFG + The SCFG object to be trnasformed and rendered. """ - ByteFlowRenderer().render_byteflow(flow).view("before") - - cflow = flow._join_returns() - ByteFlowRenderer().render_byteflow(cflow).view("closed") + scfg.view("before") - lflow = cflow._restructure_loop() - ByteFlowRenderer().render_byteflow(lflow).view("loop restructured") + scfg.join_returns() + scfg.view("closed") - bflow = lflow._restructure_branch() - ByteFlowRenderer().render_byteflow(bflow).view("branch restructured") + scfg.restructure_loop() + scfg.view("loop restructured") - -def render_scfg(scfg): - """The `render_scfg` function takes a `scfg` parameter as the SCFG - object to be transformed and rendered and renders the graphviz - representation of the SCFG. - - Parameters - ---------- - scfg: SCFG - The structured control flow graph (SCFG) to be rendered. - """ - ByteFlowRenderer().render_scfg(scfg).view("scfg") + scfg.restructure_branch() + scfg.view("branch restructured") diff --git a/numba_rvsdg/tests/simulator.py b/numba_rvsdg/tests/simulator.py index 984c3fd7..bbeea815 100644 --- a/numba_rvsdg/tests/simulator.py +++ b/numba_rvsdg/tests/simulator.py @@ -1,6 +1,5 @@ from collections import ChainMap from dis import Instruction -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow from numba_rvsdg.core.datastructures.basic_block import ( PythonBytecodeBlock, RegionBlock, @@ -23,8 +22,8 @@ class Simulator: Parameters ---------- - flow: ByteFlow - The ByteFlow to be simulated. + scfg: SCFG + The SCFG to be simulated. globals: dict of any The globals to become available during simulation @@ -49,12 +48,10 @@ class Simulator: """ - def __init__(self, flow: ByteFlow, globals: dict): - self.flow = flow - self.scfg = flow.scfg + def __init__(self, scfg, globals: dict): + self.scfg = scfg self.globals = ChainMap(globals, builtins.__dict__) - self.bcmap = {inst.offset: inst for inst in flow.bc} self.varmap = dict() self.ctrl_varmap = dict() self.stack = [] @@ -70,7 +67,7 @@ def get_block(self, name: str): `region_stack`. That is to say, if we have recursed into regions, the BasicBlock is returned from the current region (the top region of the region_stack). Otherwise the BasicBlock is returned from the initial - ByteFlow supplied to the simulator. The method `run_RegionBlock` is + SCFG supplied to the simulator. The method `run_RegionBlock` is responsible for maintaining the `region_stack`. Parameters @@ -87,9 +84,9 @@ def get_block(self, name: str): # Recursed into regions, return block from region if self.region_stack: return self.region_stack[-1].subregion[name] - # Not recursed into regions, return block from ByteFlow + # Not recursed into regions, return block from SCFG else: - return self.flow.scfg[name] + return self.scfg[name] def run(self, args): """Run the given simulator with given args. diff --git a/numba_rvsdg/tests/test_byteflow.py b/numba_rvsdg/tests/test_byteflow.py deleted file mode 100644 index a92db249..00000000 --- a/numba_rvsdg/tests/test_byteflow.py +++ /dev/null @@ -1,280 +0,0 @@ -from dis import Bytecode, Instruction, Positions - -import unittest -from numba_rvsdg.core.datastructures.basic_block import PythonBytecodeBlock -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow -from numba_rvsdg.core.datastructures.scfg import SCFG, NameGenerator -from numba_rvsdg.core.datastructures.flow_info import FlowInfo -from numba_rvsdg.core.datastructures import block_names - - -def fun(): - x = 1 - return x - - -bytecode = Bytecode(fun) -# If the function definition line changes, just change the variable below, -# rest of it will adjust as long as function remains the same -func_def_line = 11 - - -class TestBCMapFromBytecode(unittest.TestCase): - def test(self): - expected = { - 0: Instruction( - opname="RESUME", - opcode=151, - arg=0, - argval=0, - argrepr="", - offset=0, - starts_line=func_def_line, - is_jump_target=False, - positions=Positions( - lineno=func_def_line, - end_lineno=func_def_line, - col_offset=0, - end_col_offset=0, - ), - ), - 2: Instruction( - opname="LOAD_CONST", - opcode=100, - arg=1, - argval=1, - argrepr="1", - offset=2, - starts_line=func_def_line + 1, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 1, - end_lineno=func_def_line + 1, - col_offset=8, - end_col_offset=9, - ), - ), - 4: Instruction( - opname="STORE_FAST", - opcode=125, - arg=0, - argval="x", - argrepr="x", - offset=4, - starts_line=None, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 1, - end_lineno=func_def_line + 1, - col_offset=4, - end_col_offset=5, - ), - ), - 6: Instruction( - opname="LOAD_FAST", - opcode=124, - arg=0, - argval="x", - argrepr="x", - offset=6, - starts_line=func_def_line + 2, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 2, - end_lineno=func_def_line + 2, - col_offset=11, - end_col_offset=12, - ), - ), - 8: Instruction( - opname="RETURN_VALUE", - opcode=83, - arg=None, - argval=None, - argrepr="", - offset=8, - starts_line=None, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 2, - end_lineno=func_def_line + 2, - col_offset=4, - end_col_offset=12, - ), - ), - } - received = SCFG.bcmap_from_bytecode(bytecode) - self.assertEqual(expected, received) - - -class TestPythonBytecodeBlock(unittest.TestCase): - def test_constructor(self): - name_gen = NameGenerator() - block = PythonBytecodeBlock( - name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), - begin=0, - end=8, - _jump_targets=(), - backedges=(), - ) - self.assertEqual(block.name, "python_bytecode_block_0") - self.assertEqual(block.begin, 0) - self.assertEqual(block.end, 8) - self.assertFalse(block.fallthrough) - self.assertTrue(block.is_exiting) - self.assertEqual(block.jump_targets, ()) - self.assertEqual(block.backedges, ()) - - def test_is_jump_target(self): - name_gen = NameGenerator() - block = PythonBytecodeBlock( - name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), - begin=0, - end=8, - _jump_targets=( - name_gen.new_block_name(block_names.PYTHON_BYTECODE), - ), - backedges=(), - ) - self.assertEqual(block.jump_targets, ("python_bytecode_block_1",)) - self.assertFalse(block.is_exiting) - - def test_get_instructions(self): - name_gen = NameGenerator() - block = PythonBytecodeBlock( - name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), - begin=0, - end=8, - _jump_targets=(), - backedges=(), - ) - expected = [ - Instruction( - opname="RESUME", - opcode=151, - arg=0, - argval=0, - argrepr="", - offset=0, - starts_line=func_def_line, - is_jump_target=False, - positions=Positions( - lineno=func_def_line, - end_lineno=func_def_line, - col_offset=0, - end_col_offset=0, - ), - ), - Instruction( - opname="LOAD_CONST", - opcode=100, - arg=1, - argval=1, - argrepr="1", - offset=2, - starts_line=func_def_line + 1, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 1, - end_lineno=func_def_line + 1, - col_offset=8, - end_col_offset=9, - ), - ), - Instruction( - opname="STORE_FAST", - opcode=125, - arg=0, - argval="x", - argrepr="x", - offset=4, - starts_line=None, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 1, - end_lineno=func_def_line + 1, - col_offset=4, - end_col_offset=5, - ), - ), - Instruction( - opname="LOAD_FAST", - opcode=124, - arg=0, - argval="x", - argrepr="x", - offset=6, - starts_line=func_def_line + 2, - is_jump_target=False, - positions=Positions( - lineno=func_def_line + 2, - end_lineno=func_def_line + 2, - col_offset=11, - end_col_offset=12, - ), - ), - ] - - received = block.get_instructions(SCFG.bcmap_from_bytecode(bytecode)) - self.assertEqual(expected, received) - - -class TestFlowInfo(unittest.TestCase): - def test_constructor(self): - flowinfo = FlowInfo() - self.assertEqual(len(flowinfo.block_offsets), 0) - self.assertEqual(len(flowinfo.jump_insts), 0) - - def test_from_bytecode(self): - expected = FlowInfo( - block_offsets={0}, jump_insts={8: ()}, last_offset=8 - ) - - received = FlowInfo.from_bytecode(bytecode) - self.assertEqual(expected, received) - - def test_build_basic_blocks(self): - name_gen = NameGenerator() - new_name = name_gen.new_block_name(block_names.PYTHON_BYTECODE) - expected = SCFG( - graph={ - new_name: PythonBytecodeBlock( - name=new_name, - begin=0, - end=10, - _jump_targets=(), - backedges=(), - ) - } - ) - received = FlowInfo.from_bytecode(bytecode).build_basicblocks() - self.assertEqual(expected, received) - - -class TestByteFlow(unittest.TestCase): - def test_constructor(self): - byteflow = ByteFlow([], []) - self.assertEqual(len(byteflow.bc), 0) - self.assertEqual(len(byteflow.scfg), 0) - - def test_from_bytecode(self): - name_gen = NameGenerator() - new_name = name_gen.new_block_name(block_names.PYTHON_BYTECODE) - scfg = SCFG( - graph={ - new_name: PythonBytecodeBlock( - name=new_name, - begin=0, - end=10, - _jump_targets=(), - backedges=(), - ) - } - ) - expected = ByteFlow(bc=bytecode, scfg=scfg) - received = ByteFlow.from_bytecode(fun) - self.assertEqual(expected.scfg, received.scfg) - - -if __name__ == "__main__": - unittest.main() 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..63d47893 --- /dev/null +++ b/numba_rvsdg/tests/test_figures.py @@ -0,0 +1,473 @@ +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 SCFG + 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 + ), + ] + scfg = SCFG.from_bytecode(bc) + scfg.restructure() + + x, _ = SCFG.from_yaml(fig_3_yaml) + self.assertSCFGEqual(x, scfg) + + def test_figure_4(self): + # Figure 4 of the paper + + # fake bytecode just good enough for SCFG + 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 + ), + ] + scfg = SCFG.from_bytecode(bc) + scfg.restructure() + + x, _ = SCFG.from_yaml(fig_4_yaml) + self.assertSCFGEqual(x, scfg) diff --git a/numba_rvsdg/tests/test_scc.py b/numba_rvsdg/tests/test_scc.py index b3ba2d75..60169e59 100644 --- a/numba_rvsdg/tests/test_scc.py +++ b/numba_rvsdg/tests/test_scc.py @@ -1,5 +1,5 @@ -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow -from numba_rvsdg.rendering.rendering import render_flow +from numba_rvsdg.core.datastructures.scfg import SCFG +from numba_rvsdg.rendering.rendering import render_scfg def scc(G): @@ -46,14 +46,14 @@ def scc(G): return out -def make_flow(func): - return ByteFlow.from_bytecode(func) +def make_scfg(func): + return SCFG.from_bytecode(func) def test_scc(): - f = make_flow(scc) - f.restructure() + scfg = make_scfg(scc) + scfg.restructure() if __name__ == "__main__": - render_flow(make_flow(scc)) + render_scfg(make_scfg(scc)) diff --git a/numba_rvsdg/tests/test_scfg.py b/numba_rvsdg/tests/test_scfg.py index 2bb3e3b3..17bb5d72 100644 --- a/numba_rvsdg/tests/test_scfg.py +++ b/numba_rvsdg/tests/test_scfg.py @@ -1,4 +1,4 @@ -from unittest import main, TestCase +from unittest import TestCase from textwrap import dedent from numba_rvsdg.core.datastructures.scfg import SCFG, NameGenerator @@ -8,8 +8,10 @@ RegionBlock, PythonBytecodeBlock, ) -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow from numba_rvsdg.core.datastructures import block_names +from dis import Bytecode, Instruction, Positions + +import unittest class TestSCFGConversion(SCFGComparator): @@ -17,43 +19,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 +95,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": ["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"]}, }, ] @@ -106,10 +167,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) @@ -127,21 +193,226 @@ def foo(n): self.foo = foo def test_concealed_region_view_iter(self): - flow = ByteFlow.from_bytecode(self.foo) - restructured = flow._restructure_loop() + scfg = SCFG.from_bytecode(self.foo) + scfg._restructure_loop() expected = [ ("python_bytecode_block_0", PythonBytecodeBlock), ("loop_region_0", RegionBlock), ("python_bytecode_block_3", PythonBytecodeBlock), ] received = list( - ( - (k, type(v)) - for k, v in restructured.scfg.concealed_region_view.items() - ) + ((k, type(v)) for k, v in scfg.concealed_region_view.items()) + ) + self.assertEqual(expected, received) + + +def fun(): + x = 1 + return x + + +bytecode = Bytecode(fun) +# If the function definition line changes, just change the variable below, +# rest of it will adjust as long as function remains the same +func_def_line = 11 + + +class TestBCMapFromBytecode(unittest.TestCase): + def test(self): + expected = { + 0: Instruction( + opname="RESUME", + opcode=151, + arg=0, + argval=0, + argrepr="", + offset=0, + starts_line=func_def_line, + is_jump_target=False, + positions=Positions( + lineno=func_def_line, + end_lineno=func_def_line, + col_offset=0, + end_col_offset=0, + ), + ), + 2: Instruction( + opname="LOAD_CONST", + opcode=100, + arg=1, + argval=1, + argrepr="1", + offset=2, + starts_line=func_def_line + 1, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 1, + end_lineno=func_def_line + 1, + col_offset=8, + end_col_offset=9, + ), + ), + 4: Instruction( + opname="STORE_FAST", + opcode=125, + arg=0, + argval="x", + argrepr="x", + offset=4, + starts_line=None, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 1, + end_lineno=func_def_line + 1, + col_offset=4, + end_col_offset=5, + ), + ), + 6: Instruction( + opname="LOAD_FAST", + opcode=124, + arg=0, + argval="x", + argrepr="x", + offset=6, + starts_line=func_def_line + 2, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 2, + end_lineno=func_def_line + 2, + col_offset=11, + end_col_offset=12, + ), + ), + 8: Instruction( + opname="RETURN_VALUE", + opcode=83, + arg=None, + argval=None, + argrepr="", + offset=8, + starts_line=None, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 2, + end_lineno=func_def_line + 2, + col_offset=4, + end_col_offset=12, + ), + ), + } + received = SCFG.bcmap_from_bytecode(bytecode) + self.assertEqual(expected, received) + + +class TestPythonBytecodeBlock(unittest.TestCase): + def test_constructor(self): + name_gen = NameGenerator() + block = PythonBytecodeBlock( + name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), + begin=0, + end=8, + _jump_targets=(), + ) + self.assertEqual(block.name, "python_bytecode_block_0") + self.assertEqual(block.begin, 0) + self.assertEqual(block.end, 8) + self.assertFalse(block.fallthrough) + self.assertTrue(block.is_exiting) + self.assertEqual(block.jump_targets, ()) + self.assertEqual(block.backedges, ()) + + def test_is_jump_target(self): + name_gen = NameGenerator() + block = PythonBytecodeBlock( + name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), + begin=0, + end=8, + _jump_targets=( + name_gen.new_block_name(block_names.PYTHON_BYTECODE), + ), ) + self.assertEqual(block.jump_targets, ("python_bytecode_block_1",)) + self.assertFalse(block.is_exiting) + + def test_get_instructions(self): + name_gen = NameGenerator() + block = PythonBytecodeBlock( + name=name_gen.new_block_name(block_names.PYTHON_BYTECODE), + begin=0, + end=8, + _jump_targets=(), + ) + expected = [ + Instruction( + opname="RESUME", + opcode=151, + arg=0, + argval=0, + argrepr="", + offset=0, + starts_line=func_def_line, + is_jump_target=False, + positions=Positions( + lineno=func_def_line, + end_lineno=func_def_line, + col_offset=0, + end_col_offset=0, + ), + ), + Instruction( + opname="LOAD_CONST", + opcode=100, + arg=1, + argval=1, + argrepr="1", + offset=2, + starts_line=func_def_line + 1, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 1, + end_lineno=func_def_line + 1, + col_offset=8, + end_col_offset=9, + ), + ), + Instruction( + opname="STORE_FAST", + opcode=125, + arg=0, + argval="x", + argrepr="x", + offset=4, + starts_line=None, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 1, + end_lineno=func_def_line + 1, + col_offset=4, + end_col_offset=5, + ), + ), + Instruction( + opname="LOAD_FAST", + opcode=124, + arg=0, + argval="x", + argrepr="x", + offset=6, + starts_line=func_def_line + 2, + is_jump_target=False, + positions=Positions( + lineno=func_def_line + 2, + end_lineno=func_def_line + 2, + col_offset=11, + end_col_offset=12, + ), + ), + ] + + received = block.get_instructions(SCFG.bcmap_from_bytecode(bytecode)) self.assertEqual(expected, received) if __name__ == "__main__": - main() + unittest.main() diff --git a/numba_rvsdg/tests/test_simulate.py b/numba_rvsdg/tests/test_simulate.py index 68f8f44b..1e122f3b 100644 --- a/numba_rvsdg/tests/test_simulate.py +++ b/numba_rvsdg/tests/test_simulate.py @@ -1,35 +1,12 @@ -from numba_rvsdg.core.datastructures.byte_flow import ByteFlow +from numba_rvsdg.core.datastructures.scfg import SCFG from numba_rvsdg.tests.simulator import Simulator import unittest -# flow = ByteFlow.from_bytecode(foo) -# #pprint(flow.scfg) -# flow = flow.restructure() -# #pprint(flow.scfg) -# # pprint(rtsflow.scfg) -# ByteFlowRenderer().render_byteflow(flow).view() -# print(dis(foo)) -# -# sim = Simulator(flow, foo.__globals__) -# ret = sim.run(dict(x=1)) -# assert ret == foo(x=1) -# -# #sim = Simulator(flow, foo.__globals__) -# #ret = sim.run(dict(x=100)) -# #assert ret == foo(x=100) - -# You can use the following snipppet to visually debug the restructured -# byteflow: -# -# ByteFlowRenderer().render_byteflow(flow).view() -# -# - class SimulatorTest(unittest.TestCase): - def _run(self, func, flow, kwargs): + def _run(self, func, scfg, kwargs): with self.subTest(): - sim = Simulator(flow, func.__globals__) + sim = Simulator(scfg, func.__globals__) self.assertEqual(sim.run(kwargs), func(**kwargs)) def test_simple_branch(self): @@ -41,13 +18,13 @@ def foo(x): c += 1000 return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # if case - self._run(foo, flow, {"x": 1}) + self._run(foo, scfg, {"x": 1}) # else case - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) def test_simple_for_loop(self): def foo(x): @@ -56,15 +33,15 @@ def foo(x): c += i return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # loop bypass case - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) # loop case - self._run(foo, flow, {"x": 2}) + self._run(foo, scfg, {"x": 2}) # extended loop case - self._run(foo, flow, {"x": 100}) + self._run(foo, scfg, {"x": 100}) def test_simple_while_loop(self): def foo(x): @@ -75,15 +52,15 @@ def foo(x): i += 1 return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # loop bypass case - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) # loop case - self._run(foo, flow, {"x": 2}) + self._run(foo, scfg, {"x": 2}) # extended loop case - self._run(foo, flow, {"x": 100}) + self._run(foo, scfg, {"x": 100}) def test_for_loop_with_exit(self): def foo(x): @@ -94,15 +71,15 @@ def foo(x): break return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # loop bypass case - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) # loop case - self._run(foo, flow, {"x": 2}) + self._run(foo, scfg, {"x": 2}) # break case - self._run(foo, flow, {"x": 15}) + self._run(foo, scfg, {"x": 15}) def test_nested_for_loop_with_break_and_continue(self): def foo(x): @@ -118,17 +95,17 @@ def foo(x): break return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # no loop - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) # only continue - self._run(foo, flow, {"x": 1}) + self._run(foo, scfg, {"x": 1}) # no break - self._run(foo, flow, {"x": 4}) + self._run(foo, scfg, {"x": 4}) # will break - self._run(foo, flow, {"x": 5}) + self._run(foo, scfg, {"x": 5}) def test_for_loop_with_multiple_backedges(self): def foo(x): @@ -142,26 +119,26 @@ def foo(x): c += 1 return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # loop bypass - self._run(foo, flow, {"x": 0}) + self._run(foo, scfg, {"x": 0}) # default on every iteration - self._run(foo, flow, {"x": 2}) + self._run(foo, scfg, {"x": 2}) # adding 100, via the if clause - self._run(foo, flow, {"x": 4}) + self._run(foo, scfg, {"x": 4}) # adding 1000, via the elif clause - self._run(foo, flow, {"x": 7}) + self._run(foo, scfg, {"x": 7}) def test_andor(self): def foo(x, y): return (x > 0 and x < 10) or (y > 0 and y < 10) - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() - self._run(foo, flow, {"x": 5, "y": 5}) + self._run(foo, scfg, {"x": 5, "y": 5}) def test_while_count(self): def foo(s, e): @@ -172,22 +149,22 @@ def foo(s, e): i += 1 return c - flow = ByteFlow.from_bytecode(foo) - flow = flow.restructure() + scfg = SCFG.from_bytecode(foo) + scfg.restructure() # no looping - self._run(foo, flow, {"s": 0, "e": 0}) + self._run(foo, scfg, {"s": 0, "e": 0}) # single execution - self._run(foo, flow, {"s": 0, "e": 1}) + self._run(foo, scfg, {"s": 0, "e": 1}) # mutiple iterations - self._run(foo, flow, {"s": 0, "e": 5}) + self._run(foo, scfg, {"s": 0, "e": 5}) # no looping - self._run(foo, flow, {"s": 23, "e": 0}) + self._run(foo, scfg, {"s": 23, "e": 0}) # single execution - self._run(foo, flow, {"s": 23, "e": 24}) + self._run(foo, scfg, {"s": 23, "e": 24}) # mutiple iterations - self._run(foo, flow, {"s": 23, "e": 28}) + self._run(foo, scfg, {"s": 23, "e": 28}) if __name__ == "__main__": diff --git a/numba_rvsdg/tests/test_transforms.py b/numba_rvsdg/tests/test_transforms.py index 096b6cae..e8b384e5 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,50 @@ 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: + '3': ['0'] """ 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: + '3': ['0'] """ expected_scfg, expected_block_dict = SCFG.from_yaml(expected) original_scfg.insert_block( @@ -182,23 +255,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 +294,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 +330,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 +384,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 +438,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 +492,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 +563,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 +636,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 +672,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 +719,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': ['3', '1'] + '5': ['4'] + '6': ['4'] + backedges: + '4': ['1'] """ original_scfg, block_dict = SCFG.from_yaml(original) expected_scfg, _ = SCFG.from_yaml(expected) @@ -547,33 +771,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': ['3', '1'] + '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 +831,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': ['4', '1'] + '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 +895,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 +985,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..556e28c6 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,17 +46,31 @@ 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): + 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): - block_mapping[be1] = be2 - stack.append(be1) - def assertYAMLEqual( self, first_yaml: SCFG, second_yaml: SCFG, head_map: dict ): @@ -75,21 +94,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)