class SyntaxTree::YARV::ControlFlowGraph::Compiler
given instruction sequence.
This class is responsible for creating a control flow graph from the
def build_basic_blocks
Builds up a set of basic blocks by iterating over the starts of each
def build_basic_blocks block_starts = find_basic_block_starts length = 0 blocks = iseq .insns .grep(Instruction) .slice_after do |insn| length += insn.length block_starts.include?(length) end block_starts .zip(blocks) .to_h do |block_start, insns| # It's possible that we have not detected a block start but still # have branching instructions inside of a basic block. This can # happen if you have an unconditional jump which is followed by # instructions that are unreachable. As of Ruby 3.2, this is # possible with something as simple as "1 => a". In this case we # can discard all instructions that follow branching instructions. block_insns = insns.slice_after { |insn| insn.branch_targets.any? }.first [block_start, BasicBlock.new(block_start, block_insns)] end end
def compile
This method is used to compile the instruction sequence into a control
def compile blocks = build_basic_blocks connect_basic_blocks(blocks) prune_basic_blocks(blocks) ControlFlowGraph.new(iseq, insns, blocks.values).tap(&:verify) end
def connect_basic_blocks(blocks)
Connect the blocks by letting them know which blocks are incoming and
def connect_basic_blocks(blocks) blocks.each do |block_start, block| insn = block.insns.last insn.branch_targets.each do |branch_target| block.outgoing_blocks << blocks.fetch(labels[branch_target]) end if (insn.branch_targets.empty? && !insn.leaves?) || insn.falls_through? fall_through_start = block_start + block.insns.sum(&:length) block.outgoing_blocks << blocks.fetch(fall_through_start) end block.outgoing_blocks.each do |outgoing_block| outgoing_block.incoming_blocks << block end end end
def find_basic_block_starts
* fallen through to from a branch
* the target of a branch
* the start of an instruction sequence
they're either:
Finds the indices of the instructions that start a basic block because
def find_basic_block_starts block_starts = Set.new([0]) insns.each do |index, insn| branch_targets = insn.branch_targets if branch_targets.any? branch_targets.each do |branch_target| block_starts.add(labels[branch_target]) end block_starts.add(index + insn.length) if insn.falls_through? end end block_starts.to_a.sort end
def initialize(iseq)
def initialize(iseq) @iseq = iseq @insns = {} @labels = {} length = 0 iseq.insns.each do |insn| case insn when Instruction @insns[length] = insn length += insn.length when InstructionSequence::Label @labels[insn] = length end end end
def prune_basic_blocks(blocks)
If there are blocks that are unreachable, we can remove them from the
def prune_basic_blocks(blocks) visited = Set.new queue = [blocks.fetch(0)] until queue.empty? current_block = queue.shift next if visited.include?(current_block) visited << current_block queue.concat(current_block.outgoing_blocks) end blocks.select! { |_, block| visited.include?(block) } end