"""Converts 'declare-local', 'load' and 'store' instructions into SSA form.""" from collections import defaultdict import modelverse_jit.cfg_ir as cfg_ir def get_local_id(def_or_value): """Gets the node of the local resolved or declared by the given definition or value. If the given definition or value does not refer to a 'resolve-local' or 'declare-local' node, then None is returned.""" value = cfg_ir.get_def_value(def_or_value) if isinstance(value, (cfg_ir.ResolveLocal, cfg_ir.DeclareLocal)): return value.variable.node_id else: return None def get_ineligible_local_ids(entry_point): """Finds the ids of all local variables which are not eligible for conversion to SSA form.""" # Local variables are eligible for conversion to SSA form if their pointer node is never # leaked to the outside world. So we know that we can safely convert a local to SSA form # if 'resolve-local' values are only used by 'load' and 'store' values. ineligible_local_ids = set() def __maybe_mark_ineligible(def_or_value): local_id = get_local_id(def_or_value) if local_id is not None: ineligible_local_ids.add(local_id) for block in cfg_ir.get_all_blocks(entry_point): for definition in block.definitions + [block.flow]: value = cfg_ir.get_def_value(definition) if isinstance(value, cfg_ir.LoadPointer): # Loading a pointer to a local is fine. pass elif isinstance(value, cfg_ir.StoreAtPointer): # Storing a value in a local is fine, too. # But be careful not to ignore a store where the stored value is a local pointer. __maybe_mark_ineligible(value.value) else: # Walk over the dependencies, and mark them all as ineligible for # local-to-SSA conversion. for dependency in value.get_all_dependencies(): __maybe_mark_ineligible(dependency) return ineligible_local_ids def construct_ssa_form(entry_point): """Converts local variables into SSA form in the graph defined by the given entry point. A potentially new entry block is returned.""" # Build some helper data structures. all_blocks = list(cfg_ir.get_all_blocks(entry_point)) ineligible_locals = get_ineligible_local_ids(entry_point) predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point) # Create the SSA construction state. state = SSAConstructionState(all_blocks, ineligible_locals, predecessor_map) # Fill all blocks in the graph. for block in all_blocks: state.fill_block(block) # Update branches. for block in all_blocks: state.update_block_branches(block) # Nullify entry point parameters. return nullify_entry_block_parameters(entry_point) def nullify_entry_block_parameters(entry_point): """Creates or returns an entry block that takes no parameters. The (new) entry block is returned.""" # The SSA construction algorithm has the nasty habit of replacing potentially # undefined variables by entry block parameters. Codegen doesn't like that one # bit: it assumes that all block parameters are defined by the branches to those # parameters -- but there usually is no branch to the entry point! # # We can fix this by either replacing the block-parameters in the entry block by # definitions, or by creating a new entry point that jumps to the old entry # point with an argument list. The former construction is more efficient, but it is only # correct if there are no pre-existing branches to the entry point. # # We could build the predecessor map block and use the first option if possible, # but we won't; trivial phi elimination followed by block merging will reduce # the second construction to the first one anyway. if len(entry_point.parameters) == 0: return entry_point pre_entry_point = cfg_ir.BasicBlock(entry_point.counter) arg_list = [] for _ in entry_point.parameters: literal_def = pre_entry_point.append_definition(cfg_ir.Literal(None)) arg_list.append(literal_def) pre_entry_point.flow = cfg_ir.create_jump(entry_point, arg_list) return pre_entry_point # The algorithms below are based on # Simple and Efficient Construction of Static Single Assignment Form by M. Braun et al # (https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf). class SSAConstructionState(object): """Encapsulates state related to SSA construction.""" def __init__(self, all_blocks, ineligible_locals, predecessor_map): self.all_blocks = all_blocks self.ineligible_locals = ineligible_locals self.predecessor_map = predecessor_map # `current_defs` is a local node id -> basic block -> definition map. self.current_defs = defaultdict(dict) # `incomplete_phis` is a basic block -> local node id -> block parameter def map. self.incomplete_phis = defaultdict(dict) # `extra_phi_operands` is a basic block -> block parameter def -> def map. self.extra_phi_operands = defaultdict(dict) self.processed_blocks = set() self.filled_blocks = set() self.sealed_blocks = set() def read_variable(self, block, node_id): """Reads the latest definition of the local variable with the given node id for the specified block.""" if block in self.current_defs[node_id]: return self.current_defs[node_id][block] else: return self.read_variable_recursive(block, node_id) def write_variable(self, block, node_id, value): """Writes the given value to the local with the specified id in the specified block.""" self.current_defs[node_id][block] = value def read_variable_recursive(self, block, node_id): """Reads the latest definition of the local variable with the given node id from one of the given block's predecessor blocks.""" if block not in self.sealed_blocks: # Create an incomplete phi. val = block.append_parameter(cfg_ir.BlockParameter()) self.incomplete_phis[block][node_id] = val elif len(self.predecessor_map[block]) == 1: # Optimize the common case of one predecessor: no phi needed. pred = next(iter(self.predecessor_map[block])) val = self.read_variable(pred, node_id) else: # Break potential cycles with an operandless phi. val = block.append_parameter(cfg_ir.BlockParameter()) self.write_variable(block, node_id, val) val = self.add_phi_operands(node_id, val) self.write_variable(block, node_id, val) return val def add_phi_operands(self, node_id, phi_def): """Finds out which arguments branches should provide for the given block parameter definition.""" # Determine operands from predecessors all_values = [] for pred in self.predecessor_map[phi_def.block]: arg = self.read_variable(pred, node_id) self.extra_phi_operands[pred][phi_def] = arg all_values.append(arg) return self.try_remove_trivial_phi(phi_def, all_values) def try_remove_trivial_phi(self, phi_def, values): """Tries to remove a trivial block parameter definition.""" # This is a somewhat simplified (and less powerful) version of the # algorithm in the SSA construction paper. That's kind of okay, though; # trivial phi elimination is also implemented as a separate pass in the # optimization pipeline. trivial_phi_val = cfg_ir.get_trivial_phi_value(phi_def, values) if trivial_phi_val is None: return phi_def else: for pred in self.predecessor_map[phi_def.block]: del self.extra_phi_operands[pred][phi_def] phi_def.block.remove_parameter(phi_def) phi_def.redefine(trivial_phi_val) phi_def.block.prepend_definition(phi_def) return trivial_phi_val def has_sealed(self, block): """Tells if the given block has been sealed yet.""" return block in self.sealed_blocks def can_seal(self, block): """Tells if the given block can be sealed right away.""" # A block can be sealed if all if its predecessors have been filled. return all( [predecessor in self.filled_blocks for predecessor in self.predecessor_map[block]]) def seal_all_sealable_blocks(self): """Seals all sealable blocks.""" for block in self.all_blocks: if self.can_seal(block): self.seal_block(block) def seal_block(self, block): """Seals the given block.""" if self.has_sealed(block): return for node_id, phi_def in self.incomplete_phis[block].items(): self.add_phi_operands(node_id, phi_def) self.sealed_blocks.add(block) def has_filled(self, block): """Tells if the given block has been filled yet.""" return block in self.filled_blocks def fill_block(self, block): """Visits all definitions in the given block. Locals are converted into SSA form.""" if block in self.processed_blocks: return self.processed_blocks.add(block) # Try to seal the block right away if at all possible. if self.can_seal(block): self.seal_block(block) block_definitions = list(block.definitions) for definition in block_definitions: value = definition.value if cfg_ir.is_value_def(value, cfg_ir.LoadPointer): # Read the variable from the definitions dictionary. node_id = get_local_id(value.pointer) if node_id is not None and node_id not in self.ineligible_locals: definition.redefine(self.read_variable(block, node_id)) elif isinstance(value, cfg_ir.StoreAtPointer): node_id = get_local_id(value.pointer) if node_id is not None and node_id not in self.ineligible_locals: # Write to the variable, and replace the definition by a 'None' literal. self.write_variable(block, node_id, value.value) definition.redefine(cfg_ir.Literal(None)) elif isinstance(value, cfg_ir.DeclareLocal): node_id = value.variable.node_id if node_id not in self.ineligible_locals: definition.redefine(cfg_ir.Literal(None)) # Mark the block as filled. self.filled_blocks.add(block) # Seal all sealable blocks. self.seal_all_sealable_blocks() # Fill successor blocks. for branch in block.flow.branches(): self.fill_block(branch.block) def update_block_branches(self, block): """Appends arguments to the given block's flow instruction's branches, if necessary.""" for branch in block.flow.branches(): # Find all pairs phis which are defined in the branch target block. applicable_pairs = [ (phi_def, operand_def) for phi_def, operand_def in self.extra_phi_operands[block].items() if phi_def.block == branch.block] if len(applicable_pairs) == 0: # We might as well early-out here. continue # Sort the pairs by block parameter index. sorted_pairs = sorted( applicable_pairs, key=lambda (phi_def, _): phi_def.block.parameters.index(phi_def)) # Append arguments to the branch. for _, arg in sorted_pairs: branch.arguments.append(arg)