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 Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ gem "rspec", "~> 3.0"
gem "stackprof"
gem "standard"
gem "ruby-prof"

gem "benchmark-ips"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
benchmark-ips (2.9.2)
diff-lcs (1.4.4)
parallel (1.21.0)
parser (3.0.2.0)
Expand Down Expand Up @@ -54,6 +55,7 @@ PLATFORMS
ruby

DEPENDENCIES
benchmark-ips
dead_end!
rake (~> 12.0)
rspec (~> 3.0)
Expand Down
2 changes: 1 addition & 1 deletion lib/dead_end/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def self.valid?(source)
require_relative "code_block"
require_relative "block_expand"
require_relative "ripper_errors"
require_relative "insertion_sort"
require_relative "priority_queue"
require_relative "around_block_scan"
require_relative "pathname_from_message"
require_relative "display_invalid_blocks"
Expand Down
21 changes: 12 additions & 9 deletions lib/dead_end/code_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,22 @@ module DeadEnd
#
class CodeBlock
UNSET = Object.new.freeze
attr_reader :lines
attr_reader :lines, :starts_at, :ends_at

def initialize(lines: [])
@lines = Array(lines)
@valid = UNSET
@deleted = false
@starts_at = @lines.first.number
@ends_at = @lines.last.number
end

def delete
@deleted = true
end

def deleted?
@deleted
end

def visible_lines
Expand All @@ -41,14 +52,6 @@ def hidden?
@lines.all?(&:hidden?)
end

def starts_at
@starts_at ||= @lines.first&.line_number
end

def ends_at
@ends_at ||= @lines.last&.line_number
end

# This is used for frontier ordering, we are searching from
# the largest indentation to the smallest. This allows us to
# populate an array with multiple code blocks then call `sort!`
Expand Down
38 changes: 27 additions & 11 deletions lib/dead_end/code_frontier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module DeadEnd
class CodeFrontier
def initialize(code_lines:)
@code_lines = code_lines
@frontier = InsertionSort.new
@frontier = PriorityQueue.new
@unvisited_lines = @code_lines.sort_by(&:indent_index)
@visited_lines = {}

Expand All @@ -61,7 +61,7 @@ def initialize(code_lines:)
end

def count
@frontier.to_a.length
@frontier.length
end

# Performance optimization
Expand Down Expand Up @@ -103,23 +103,23 @@ def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)

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

def next_indent_line
@unvisited_lines.last
end

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

frontier_indent = @frontier.to_a.last.current_indent
frontier_indent = @frontier.peek.current_indent
unvisited_indent = next_indent_line.indent

if ENV["DEBUG"]
puts "```"
puts @frontier.to_a.last.to_s
puts @frontier.peek.to_s
puts "```"
puts " @frontier indent: #{frontier_indent}"
puts " @unvisited indent: #{unvisited_indent}"
Expand All @@ -129,6 +129,8 @@ def expand?
frontier_indent >= unvisited_indent
end

# 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]
Expand All @@ -140,18 +142,32 @@ def register_indent_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 block.starts_at != block.ends_at # A block of size 1 cannot engulf another since
@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
#
# This method ensures the frontier always remains sorted (in indentation order)
# 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)

# Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
@frontier.to_a.reject! { |b|
b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
}
register_engulf_block(block)

@check_next = true if block.invalid?
@frontier << block
Expand Down
46 changes: 0 additions & 46 deletions lib/dead_end/insertion_sort.rb

This file was deleted.

105 changes: 105 additions & 0 deletions lib/dead_end/priority_queue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module DeadEnd
# Holds elements in a priority heap on insert
#
# Instead of constantly calling `sort!`, put
# the element where it belongs the first time
# around
#
# Example:
#
# sorted = InsertionSort.new
# sorted << 33
# sorted << 44
# sorted << 1
# puts sorted.to_a
# # => [1, 44, 33]
#
class PriorityQueue
attr_reader :elements

def initialize
@elements = []
end

def <<(element)
@elements << element
bubble_up(last_index, element)
end

def pop
exchange(0, last_index)
max = @elements.pop
bubble_down(0)
max
end

def length
@elements.length
end

def empty?
@elements.empty?
end

def peek
@elements.first
end

def to_a
@elements
end

# Used for testing, extremely not performant
def sorted
out = []
elements = @elements.dup
while (element = pop)
out << element
end
@elements = elements
out.reverse
end

private def last_index
@elements.size - 1
end

private def bubble_up(index, element)
return if index <= 0

parent_index = (index - 1) / 2
parent = @elements[parent_index]

return if (parent <=> element) >= 0

exchange(index, parent_index)
bubble_up(parent_index, element)
end

private def bubble_down(index)
child_index = (index * 2) + 1

return if child_index > last_index

not_the_last_element = child_index < last_index
left_element = @elements[child_index]
right_element = @elements[child_index + 1]

child_index += 1 if not_the_last_element && (right_element <=> left_element) == 1

return if (@elements[index] <=> @elements[child_index]) >= 0

exchange(index, child_index)
bubble_down(child_index)
end

def exchange(source, target)
a = @elements[source]
b = @elements[target]
@elements[source] = b
@elements[target] = a
end
end
end
6 changes: 4 additions & 2 deletions spec/unit/code_frontier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module DeadEnd
frontier << b
end

expect(frontier.detect_invalid_blocks).to eq(blocks)
expect(frontier.detect_invalid_blocks.sort).to eq(blocks.sort)
end

it "self.combination" do
Expand Down Expand Up @@ -61,13 +61,15 @@ def foo
expect(frontier.count).to eq(1)

frontier << CodeBlock.new(lines: [code_lines[1], code_lines[2], code_lines[3]])
expect(frontier.count).to eq(1)
# expect(frontier.count).to eq(1)
expect(frontier.pop.to_s).to eq(<<~EOM.indent(2))
puts "lol"
puts "lol"
puts "lol"
EOM

expect(frontier.pop).to be_nil

code_lines = code_line_array(<<~EOM)
def foo
puts "lol"
Expand Down
49 changes: 0 additions & 49 deletions spec/unit/insertion_sort_spec.rb

This file was deleted.

Loading