Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/dead_end/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ def self.valid?(source)
require_relative "block_expand"
require_relative "ripper_errors"
require_relative "priority_queue"
require_relative "unvisited_lines"
require_relative "around_block_scan"
require_relative "priority_engulf_queue"
require_relative "pathname_from_message"
require_relative "display_invalid_blocks"
require_relative "parse_blocks_from_indent_line"
57 changes: 16 additions & 41 deletions lib/dead_end/code_frontier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,16 @@ module DeadEnd
# CodeFrontier#detect_invalid_blocks
#
class CodeFrontier
def initialize(code_lines:)
def initialize(code_lines:, unvisited: UnvisitedLines.new(code_lines: code_lines))
@code_lines = code_lines
@frontier = PriorityQueue.new
@unvisited_lines = @code_lines.sort_by(&:indent_index)
@visited_lines = {}
@unvisited = unvisited
@queue = PriorityEngulfQueue.new

@has_run = false
@check_next = true
end

def count
@frontier.length
@queue.length
end

# Performance optimization
Expand All @@ -88,7 +86,7 @@ def count
# removed. By default it checks all blocks in present in
# the frontier array, but can be used for arbitrary arrays
# of codeblocks as well
def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)
def holds_all_syntax_errors?(block_array = @queue, can_cache: true)
return false if can_cache && can_skip_check?

without_lines = block_array.to_a.flat_map do |block|
Expand All @@ -103,23 +101,23 @@ def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)

# Returns a code block with the largest indentation possible
def pop
@frontier.pop
@queue.pop
end

def next_indent_line
@unvisited_lines.last
@unvisited.peek
end

def expand?
return false if @frontier.empty?
return true if @unvisited_lines.to_a.empty?
return false if @queue.empty?
return true if @unvisited.empty?

frontier_indent = @frontier.peek.current_indent
frontier_indent = @queue.peek.current_indent
unvisited_indent = next_indent_line.indent

if ENV["DEBUG"]
puts "```"
puts @frontier.peek.to_s
puts @queue.peek.to_s
puts "```"
puts " @frontier indent: #{frontier_indent}"
puts " @unvisited indent: #{unvisited_indent}"
Expand All @@ -132,37 +130,14 @@ def expand?
# Keeps track of what lines have been added to blocks and which are not yet
# visited.
def register_indent_block(block)
block.lines.each do |line|
next if @visited_lines[line]
@visited_lines[line] = true

index = @unvisited_lines.bsearch_index { |l| line.indent_index <=> l.indent_index }
@unvisited_lines.delete_at(index)
end
@unvisited.visit_block(block)
self
end

# When one element fully encapsulates another we remove the smaller
# block from the frontier. This prevents double expansions and all-around
# weird behavior. However this guarantee is quite expensive to maintain
def register_engulf_block(block)
# If we're about to pop off the same block, we can skip deleting
# things from the frontier this iteration since we'll get it
# on the next iteration
return if @frontier.peek && (block <=> @frontier.peek) == 1

if block.starts_at != block.ends_at # A block of size 1 cannot engulf another
@frontier.to_a.each { |b|
if b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
b.delete
true
end
}
end

while (last = @frontier.peek) && last.deleted?
@frontier.pop
end
end

# Add a block to the frontier
Expand All @@ -171,11 +146,11 @@ def register_engulf_block(block)
# and that each code block's lines are removed from the indentation hash so we
# don't re-evaluate the same line multiple times.
def <<(block)
register_indent_block(block)
register_engulf_block(block)
@unvisited.visit_block(block)

@queue.push(block)

@check_next = true if block.invalid?
@frontier << block

self
end
Expand All @@ -195,7 +170,7 @@ def self.combination(array)
# Given that we know our syntax error exists somewhere in our frontier, we want to find
# the smallest possible set of blocks that contain all the syntax errors
def detect_invalid_blocks
self.class.combination(@frontier.to_a.select(&:invalid?)).detect do |block_array|
self.class.combination(@queue.to_a.select(&:invalid?)).detect do |block_array|
holds_all_syntax_errors?(block_array, can_cache: false)
end || []
end
Expand Down
63 changes: 63 additions & 0 deletions lib/dead_end/priority_engulf_queue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module DeadEnd
# Keeps track of what elements are in the queue in
# priority and also ensures that when one element
# engulfs/covers/eats another that the larger element
# evicts the smaller element
class PriorityEngulfQueue
def initialize
@queue = PriorityQueue.new
end

def to_a
@queue.to_a
end

def empty?
@queue.empty?
end

def length
@queue.length
end

def peek
@queue.peek
end

def pop
@queue.pop
end

def push(block)
prune_engulf(block)
@queue << block
flush_deleted

self
end

private def flush_deleted
while @queue&.peek&.deleted?
@queue.pop
end
end

private def prune_engulf(block)
# If we're about to pop off the same block, we can skip deleting
# things from the frontier this iteration since we'll get it
# on the next iteration
return if @queue.peek && (block <=> @queue.peek) == 1

if block.starts_at != block.ends_at # A block of size 1 cannot engulf another
@queue.to_a.each { |b|
if b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
b.delete
true
end
}
end
end
end
end
36 changes: 36 additions & 0 deletions lib/dead_end/unvisited_lines.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module DeadEnd
# Tracks which lines various code blocks have expanded to
# and which are still unexplored
class UnvisitedLines
def initialize(code_lines:)
@unvisited = code_lines.sort_by(&:indent_index)
@visited_lines = {}
@visited_lines.compare_by_identity
end

def empty?
@unvisited.empty?
end

def peek
@unvisited.last
end

def pop
@unvisited.pop
end

def visit_block(block)
block.lines.each do |line|
next if @visited_lines[line]
@visited_lines[line] = true
end

while @visited_lines[@unvisited.last]
@unvisited.pop
end
end
end
end