123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- """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)
|