Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Kaigi] Beware the Dead End

Richard Schneeman
September 20, 2021
65

[Kaigi] Beware the Dead End

Nothing stops a program from executing quite as fast as a syntax error. After years of “unexpected end” in my dev life, I decided to “do” something about it. In this talk we'll cover lexing, parsing, and indentation informed syntax tree search that power that dead_end Ruby library.

PS I also gave this talk at RubyConf, but with a whole bunch of new content. I recommend that one instead.

Richard Schneeman

September 20, 2021
Tweet

Transcript

  1. Beware
    the Dreaded
    Dead End!!!
    By @schneems

    View Slide

  2. View Slide

  3. View Slide

  4. Beware
    the Dreaded
    Dead End!!!
    By @schneems

    View Slide

  5. >
    # <= routes.rb:121: syntax error, unexpected end-of-input, expecting `end’
    Rails.application.routes.draw do


    constraints -> { Rails.application.config.non_production } do


    namespace :foo do


    resource :bar


    end


    end


    constraints -> { Rails.application.config.non_production } do


    namespace :bar do


    resource :baz


    end


    end


    namespace :admin do


    resource :session


    match "/foobar(*path)", via: :all, to: redirect { |_params, req|


    uri = URI(req.path.gsub("foobar", "foobaz"))


    uri.query = req.query_string.presence


    uri.to_s


    }


    end

    View Slide

  6. >
    Rails.application.routes.draw do


    constraints -> { Rails.application.config.non_production } do


    namespace :foo do


    resource :bar


    end


    end


    constraints -> { Rails.application.config.non_production } do


    namespace :bar do


    resource :baz


    end


    end


    namespace :admin do


    resource :session


    match "/foobar(*path)", via: :all, to: redirect { |_params, req|


    uri = URI(req.path.gsub("foobar", "foobaz"))


    uri.query = req.query_string.presence


    uri.to_s


    }


    end # <= routes.rb:121: syntax error, unexpected end-of-input, expecting `end’

    View Slide

  7. >
    RSpec.describe Cutlass::BashResult do


    it "preserves stdout, stderr, and status"


    stdout = SecureRandom.hex(16)


    stderr = SecureRandom.hex(16)


    status = 0


    result = BashResult.new(


    stdout: stdout,


    stderr: stderr,


    status: status


    )


    expect(result.stdout).to eq(stdout)


    expect(result.stderr).to eq(stderr)


    expect(result.status).to eq(status)


    end


    end # <= result_spec:19 syntax error, unexpected end-of-input, expecting `end’

    View Slide

  8. 🤔

    View Slide

  9. > 3 module Cutlass


    4 RSpec.describe Cutlass::BashResult do


    ❯ 5 it "preserves stdout, stderr, and status"


    ❯ 19 end


    21 it "success?" do


    52 end


    53 end


    54 end
    This code has an unmatched `end`. Ensure that all
    `end` lines in your code have a matching syntax
    keyword (`def`, `do`, etc.)

    View Slide

  10. >

    View Slide

  11. >
    # frozen_string_literal: true


    module Cutlass


    RSpec.describe Cutlass::BashResult do


    it "preserves stdout, stderr, and status"


    stdout = SecureRandom.hex(16)


    stderr = SecureRandom.hex(16)


    status = 0


    result = BashResult.new(


    stdout: stdout,


    stderr: stderr,


    status: status


    )


    expect(result.stdout).to eq(stdout)


    expect(result.stderr).to eq(stderr)


    expect(result.status).to eq(status)


    end


    end


    end # <= result_spec:19 syntax error, unexpected end-of-input, expecting `end’

    View Slide

  12. >
    3 module Cutlass


    4 RSpec.describe Cutlass::BashResult do


    ❯ 5 it "preserves stdout, stderr, and status"


    ❯ 19 end


    21 it "success?" do


    52 end


    53 end


    54 end

    View Slide

  13. 0
    25
    50
    75
    100
    April May June July
    (I did not do a study)

    View Slide

  14. (This is a lie)
    -30
    -15
    0
    15
    30
    45
    60
    April May June July August

    View Slide

  15. (Also a lie)
    7%
    8%
    11%
    11%
    31%
    32%

    View Slide

  16. 🥰

    View Slide

  17. $ gem install dead_end

    View Slide

  18. >
    3 module Cutlass


    4 RSpec.describe Cutlass::BashResult do


    ❯ 5 it "preserves stdout, stderr, and status"


    ❯ 19 end


    21 it "success?" do


    52 end


    53 end


    54 end
    DeadEnd: Unmatched `end` detected

    View Slide

  19. > 1 module Cutlass


    2 RSpec.describe Cutlass::BashResult do


    ❯ 3 it "preserves stdout, stderr, and status" do


    5 it "success?" do


    6 end


    7 end


    8 end
    DeadEnd: Missing `end` detected

    View Slide

  20. >
    1 class Cat


    2 def to_json


    ❯ 3 hash = {


    ❯ 4 name: name,


    ❯ 5 fur_color: fur_color,


    ❯ 6 type: 'Cat',


    ❯ 7 better_than_dog: false


    ❯ 8 hash.to_json


    9 end


    10 end
    DeadEnd: Unmatched `}` character detected

    View Slide

  21. >
    1 class Animal


    2 def self.ones_my_three_year_old_likes


    ❯ 3 array =


    ❯ 4 Dog.new,


    ❯ 5 Lion.new,


    ❯ 6 Tiger.new,


    ❯ 7 Aligator.new,


    ❯ 8 ]


    10 return array


    11 end


    12 end
    DeadEnd: Unmatched `[` detected

    View Slide

  22. > 1 class Cat


    2 def meow


    ❯ 3 Animal.call do |a


    ❯ 5 end


    6 end


    7 end
    DeadEnd: Unmatched `|` character detected

    View Slide

  23. > 1 class Cat


    2 def meow


    ❯ 3 Animal.call do |a


    ❯ 5 end


    6 end


    7 end
    DeadEnd: Unmatched `|` character detected
    ?
    ?
    ?

    View Slide

  24. $ gem install dead_end

    View Slide

  25. Syntax
    errors

    View Slide

  26. Lexing &


    Parsing

    View Slide

  27. AI

    View Slide

  28. dead_end

    View Slide

  29. Internals

    View Slide

  30. 🤚

    View Slide

  31. @schneems

    View Slide

  32. @schneems

    View Slide

  33. View Slide

  34. 📕
    How to
    Open Source

    View Slide

  35. Heroku

    View Slide

  36. Heroku

    View Slide

  37. NoMemoryError

    ScriptError

    LoadError

    SyntaxError

    SecurityError

    SignalException

    KeyError

    StandardError

    ArgumentError

    EncodingError

    IOError

    EOFError
    I am Exceptional

    View Slide

  38. >
    Dir.chhhdir("/tmp") do


    puts `ls`


    end

    View Slide

  39. >
    Dir.chhhdir("/tmp") do


    puts `ls`


    end
    # unde
    fi
    ned method `chhhdir' for


    # Dir:Class (NoMethodError
    )

    # Did you mean? chdir

    View Slide

  40. >

    View Slide

  41. require 'bundler'


    require 'date'


    require 'optparse'


    require 'ostruct'


    require 'shellwords'


    class Rexe


    VERSION = '1.5.1'


    PROJECT_URL = 'https://github.com/keithrbennett/rexe'


    module Helpers


    # Try executing code. If error raised, print message (but not stack trace) & exit -1.


    def try


    begin



    View Slide

  42. end


    def bundler_run(&block)


    # This used to be an unconditional call to with_clean_env but that method is now deprecated:


    # [DEPRECATED] `Bundler.with_clean_env` has been deprecated in favor of `Bundler.with_unbundled_env`.


    # If you instead want the environment before bundler was originally loaded,


    # use `Bundler.with_original_env`


    if Bundler.respond_to?(:with_unbundled_env)


    Bundler.with_unbundled_env { block.call }


    else


    Bundler.with_clean_env { block.call }


    end


    end


    bundler_run { Rexe::Main.new.call }


    View Slide

  43. if / end

    unless / end

    while / end

    until / end

    def / end

    for / end

    begin / end

    class / end

    module / end

    do / end

    View Slide

  44. >
    Dir.chdir("lol")


    puts `ls`


    end
    ☠☠☠

    View Slide

  45. >
    Dir.chdir("lol")


    puts `ls`


    end
    ??

    View Slide

  46. >
    Dir.chdir("/tmp")


    Dir.chdir("lol") do


    puts `ls`


    end

    View Slide

  47. >
    def bark


    puts "woof"


    end


    bark()


    # => "woof"

    View Slide

  48. >
    def bark


    puts "woof"


    end


    bark do


    puts "not a syntax error"


    end


    # => "woof"

    View Slide

  49. >
    expect(WebMock).to


    have_requested(


    :post,


    "localhost:#{port}"


    ).


    end
    $ ruby -wc scratch.rb


    Syntax OK

    View Slide

  50. 📓

    View Slide

  51. Lexing &


    Parsing

    View Slide

  52. "hello world"
    Ripper.lex
    >

    View Slide

  53. View Slide

  54. "hello world"
    Ripper.lex
    [


    [[1, 0], :on_tstring_beg, "\"", BEG],


    [[1, 1], :on_tstring_content, "hello world", BEG],


    [[1, 12], :on_tstring_end, "\"", END],


    [[1, 13], :on_nl, "\n", BEG]


    ]
    >

    View Slide

  55. [


    [[1, 0], :on_tstring_beg, "\"", BEG],


    [[1, 1], :on_tstring_content, "hello world", BEG],


    [[1, 12], :on_tstring_end, "\"", END],


    [[1, 13], :on_nl, "\n", BEG]


    ]

    View Slide

  56. [


    [[1, 0], :on_tstring_beg, "\"", BEG],


    [[1, 1], :on_tstring_content, "hello world", BEG],


    [[1, 12], :on_tstring_end, "\"", END],


    [[1, 13], :on_nl, "\n", BEG]


    ]

    View Slide

  57. [


    [[1, 0], :on_tstring_beg, "\"", BEG],


    [[1, 1], :on_tstring_content, "hello world", BEG],


    [[1, 12], :on_tstring_end, "\"", END],


    [[1, 13], :on_nl, "\n", BEG]


    ]

    View Slide

  58. while b != 0


    if a > b


    a = a - b


    else


    b = b - a


    end


    end


    return a
    Ripper.parse

    View Slide

  59. while b != 0


    # if a > b


    a = a - b


    else


    b = b - a


    end


    end


    return a
    Ripper.parse

    :(

    View Slide

  60. 🤔

    View Slide

  61. while b != 0


    # if a > b


    a = a - b


    else


    b = b - a


    end


    end


    return a
    Ripper.parse

    :(

    View Slide

  62. while b != 0


    # if a > b


    # a = a - b


    # else


    # b = b - a


    # end


    end


    return a
    Ripper.parse

    :)

    View Slide

  63. while b != 0


    # if a > b


    # a = a - b


    # else


    # b = b - a


    # end


    end


    return a
    Ripper.parse

    :)

    View Slide

  64. AI

    View Slide

  65. Artificial Intelligence
    Algorithm

    View Slide

  66. Artificial Intelligence
    Algorithm

    View Slide

  67. Pathfinding
    Credit: Factorio 'New
    pathfinding algorithm'

    View Slide

  68. Credit @redblobgames: Introduction to the A-star Algorithm

    View Slide

  69. Pathfinding
    Credit: Factorio 'New
    pathfinding algorithm'

    View Slide

  70. Tree

    Search

    "Reti Draw" Endgame

    Chess problem by

    Richard Reti

    (1921)

    View Slide

  71. Tree

    Search

    BOTVINNIK, M.M.

    (1984).

    "Computers in Chess
    Solving Inexact Search
    Problems"

    View Slide

  72. Non-optimal
    Play

    View Slide

  73. View Slide

  74. View Slide

  75. View Slide

  76. View Slide

  77. View Slide

  78. View Slide

  79. View Slide

  80. View Slide

  81. View Slide

  82. View Slide

  83. View Slide

  84. View Slide

  85. View Slide

  86. View Slide

  87. Checkmate
    Black

    View Slide

  88. Optimal
    Draw
    Play

    View Slide

  89. View Slide

  90. View Slide

  91. View Slide

  92. View Slide

  93. View Slide

  94. View Slide

  95. View Slide

  96. View Slide

  97. Draw

    View Slide

  98. Tree

    Search

    BOTVINNIK, M.M.

    (1984).

    "Computers in Chess
    Solving Inexact Search
    Problems"

    View Slide

  99. AI ~ Search

    View Slide

  100. View Slide

  101. while b != 0


    if a > b


    a = a - b


    else


    b = b - a


    end


    end


    return a
    Ripper.parse

    View Slide

  102. while b != 0


    # if a > b


    a = a - b


    else


    b = b - a


    end


    end


    return a
    Ripper.parse

    View Slide

  103. ❌🌳💔

    View Slide

  104. >
    Dir.chdir("/tmp")


    Dir.chdir("lol")


    end
    do???
    do???
    puts `ls`

    View Slide

  105. >
    Dir.chdir("/tmp")


    Dir.chdir("lol")


    end
    do
    puts `ls`🥰

    View Slide

  106. while b != 0


    # if a > b


    # a = a - b


    # else


    # b = b - a


    # end


    end


    return a
    Ripper.parse

    View Slide

  107. 🌳
    ❌ 💔

    View Slide

  108. demo

    View Slide

  109. module SyntaxErrorSearch


    # Used for formatting invalid blocks


    class DisplayInvalidBlocks


    attr_reader :filename


    def initialize(block_array, io: $stderr, filename: nil)


    @filename = filename


    @io = io


    @blocks = block_array


    @lines = @blocks.map(&:lines).flatten


    @digit_count = @lines.last.line_number.to_s.length


    @code_lines = @blocks.first.code_lines


    @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    end


    def call



    64: syntax error, unexpected end-of-input,
    expecting `end'
    📕

    View Slide

  110. 51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    58 string << "\e[0m"


    59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  111. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  112. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  113. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  114. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  115. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  116. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    ❯ 54 string = String.new


    ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  117. 50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    📕

    View Slide

  118. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    ❯ 52 "#{number.to_s}#{line}"


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  119. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    📕

    View Slide

  120. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    ❯ 51 if line.empty?


    ❯ 53 else


    ❯ 60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  121. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    61 end.join


    62 end


    63 end


    64 end


    📕

    View Slide

  122. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    ❯ 49 next if line.hidden?


    ❯ 50 number = line.line_number.to_s.rjust(@digit_count)


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  123. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    61 end.join


    62 end


    63 end


    64 end


    📕

    View Slide

  124. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    ❯ 48 @code_lines.map do |line|


    ❯ 61 end.join


    62 end


    63 end


    64 end


    View Slide

  125. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    62 end


    63 end


    64 end


    📕

    View Slide

  126. 42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    ❯ 47 def code_with_lines


    ❯ 62 end


    63 end


    64 end


    View Slide

  127. 40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    63 end


    64 end


    📕

    View Slide

  128. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    63 end


    64 end


    View Slide

  129. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  130. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    ❯ 43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  131. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    ❯ 42 string << code_with_lines


    ❯ 43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  132. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    ❯ 42 string << code_with_lines


    ❯ 43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  133. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    ❯ 40 string << "```\n"


    ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    ❯ 42 string << code_with_lines


    ❯ 43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  134. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    ❯ 39 string = String.new("")


    ❯ 40 string << "```\n"


    ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    ❯ 42 string << code_with_lines


    ❯ 43 string << "```\n"


    ❯ 44 string


    45 end


    46


    63 end


    64 end


    View Slide

  135. 13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    45 end


    46


    63 end


    64 end


    📕

    View Slide

  136. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    9 @blocks = block_array


    10 @lines = @blocks.map(&:lines).flatten


    11 @digit_count = @lines.last.line_number.to_s.length


    12 @code_lines = @blocks.first.code_lines


    13


    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37



    View Slide

  137. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    9 @blocks = block_array


    10 @lines = @blocks.map(&:lines).flatten


    11 @digit_count = @lines.last.line_number.to_s.length


    12 @code_lines = @blocks.first.code_lines


    13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename


    37



    View Slide

  138. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    9 @blocks = block_array


    10 @lines = @blocks.map(&:lines).flatten


    11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  139. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    9 @blocks = block_array


    10 @lines = @blocks.map(&:lines).flatten


    ❯ 11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  140. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    9 @blocks = block_array


    ❯ 10 @lines = @blocks.map(&:lines).flatten


    ❯ 11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  141. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    8 @io = io


    ❯ 9 @blocks = block_array


    ❯ 10 @lines = @blocks.map(&:lines).flatten


    ❯ 11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  142. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    7 @filename = filename


    ❯ 8 @io = io


    ❯ 9 @blocks = block_array


    ❯ 10 @lines = @blocks.map(&:lines).flatten


    ❯ 11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  143. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    ❯ 7 @filename = filename


    ❯ 8 @io = io


    ❯ 9 @blocks = block_array


    ❯ 10 @lines = @blocks.map(&:lines).flatten


    ❯ 11 @digit_count = @lines.last.line_number.to_s.length


    ❯ 12 @code_lines = @blocks.first.code_lines


    ❯ 13


    ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    35


    36 def filename





    View Slide

  144. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    15 end


    16


    35


    36 def filename


    37


    38 def code_with_filename


    45 end


    46


    63 end


    64 end


    📕

    View Slide

  145. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    15 end


    16


    35


    36 def filename


    37


    ❯ 38 def code_with_filename


    ❯ 45 end


    46


    63 end


    64 end


    View Slide

  146. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    6 def initialize(block_array, io: $stderr, filename: nil)


    15 end


    16


    35


    36 def filename


    37


    46


    63 end


    64 end


    📕

    View Slide

  147. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    ❯ 6 def initialize(block_array, io: $stderr, filename: nil)


    ❯ 15 end


    16


    35


    36 def filename


    37


    46


    63 end


    64 end


    View Slide

  148. 📕

    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    5


    16


    35


    36 def filename


    37


    46


    63 end


    64 end


    View Slide

  149. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    ❯ 5


    ❯ 16


    ❯ 35


    ❯ 36 def filename


    ❯ 37


    ❯ 46


    63 end


    64 end


    View Slide

  150. 1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    63 end


    64 end


    📕

    View Slide

  151. Syntax O
    K

    🥰🥰🥰
    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    63 end


    64 end


    📕

    View Slide

  152. DeadEnd: Missing `end` detected


    This code has a missing `end`. Ensure that all


    syntax keywords (`def`, `do`, etc.) have a matching `end`.


    file: /Users/rschneeman/Documents/projects/dead_end/spec/fixtures/this_project_extra_def.rb.txt


    simplified:


    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    ❯ 36 def filename


    ❯ 38 def code_with_filename


    ❯ 45 end


    63 end


    64 end

    View Slide

  153. Internals

    View Slide

  154. module Kernel


    module_function


    def require(file)


    dead_end_original_require(file)


    rescue SyntaxError => e


    DeadEnd.handle_error(e)


    end


    end
    dead_end/auto.rb

    View Slide

  155. # ...


    Timeout.timeout(timeout) do


    record_dir ||= ENV["DEBUG"] ? "tmp" : nil


    search = CodeSearch.new(source, record_dir: record_dir).call


    end


    # ...
    dead_end/internals.rb

    View Slide

  156. # ...


    Timeout.timeout(timeout) do


    record_dir ||= ENV["DEBUG"] ? "tmp" : nil


    search = CodeSearch.new(source, record_dir: record_dir).call


    end


    # ...

    View Slide



  157. #


    # ## Syntax error detection


    #


    # When the frontier holds the syntax error, we can stop searching


    #


    # search = CodeSearch.new(<<~EOM)


    # def dog


    # def lol


    # end


    # EOM


    #


    # search.call


    #


    # search.invalid_blocks.map(&:to_s) # =>


    # # => ["def lol\n"]


    #



    code_search.rb

    View Slide

  158. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end


    @invalid_blocks.concat(frontier.detect_invalid_blocks )




    code_search.rb

    View Slide

  159. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  160. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end





    code_search.rb
    📕

    📕

    View Slide

  161. >
    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    ❯ 5


    ❯ 16


    ❯ 35


    ❯ 36 def filename


    ❯ 37


    ❯ 46


    63 end


    64 end


    📕

    View Slide

  162. >
    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    63 end


    64 end


    📕

    View Slide

  163. # ...


    end


    module DeadEnd


    # ...


    def self.invalid?(source)


    source = source.join if source.is_a?(Array)


    source = source.to_s


    Ripper.new(source).tap(&:parse).error?


    end


    # ...


    end
    dead_end.rb

    View Slide

  164. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    58 string << "\e[0m"


    59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  165. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end





    code_search.rb

    View Slide

  166. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end


    @invalid_blocks.concat(frontier.detect_invalid_blocks )




    code_search.rb

    View Slide

  167. def expand?


    return false if @frontier.empty?


    # ...


    # Expand all blocks before moving to unvisited lines


    frontier_indent >= unvisited_indent


    end


    code_frontier.rb

    View Slide

  168. def expand?


    return false if @frontier.empty?


    # ...


    # Expand all blocks before moving to unvisited lines


    frontier_indent >= unvisited_indent


    end


    code_frontier.rb

    View Slide

  169. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end


    @invalid_blocks.concat(frontier.detect_invalid_blocks )




    code_search.rb

    View Slide

  170. end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end


    @invalid_blocks.concat(frontier.detect_invalid_blocks )




    code_search.rb

    View Slide

  171. # Parses the most indented lines into blocks that are marked


    # and added to the frontier


    def visit_new_blocks


    max_indent = frontier.next_indent_line&.indent


    while (line = frontier.next_indent_line) && (line.indent == max_indent)


    @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|


    record(block: block, name: "add")


    block.mark_invisible if block.valid?


    push(block, name: "add")


    end


    end


    end



    code_search.rb

    View Slide

  172. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  173. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  174. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  175. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  176. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    54 string = String.new


    ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  177. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    ❯ 54 string = String.new


    ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  178. # Parses the most indented lines into blocks that are marked


    # and added to the frontier


    def visit_new_blocks


    max_indent = frontier.next_indent_line&.indent


    while (line = frontier.next_indent_line) && (line.indent == max_indent)


    @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|


    record(block: block, name: "add")


    block.mark_invisible if block.valid?


    push(block, name: "add")


    end


    end


    end



    code_search.rb

    View Slide

  179. # Parses the most indented lines into blocks that are marked


    # and added to the frontier


    def visit_new_blocks


    max_indent = frontier.next_indent_line&.indent


    while (line = frontier.next_indent_line) && (line.indent == max_indent)


    @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|


    record(block: block, name: "add")


    block.mark_invisible if block.valid?


    push(block, name: "add")


    end


    end


    end



    code_search.rb

    View Slide

  180. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    ❯ 54 string = String.new


    ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics


    ❯ 56 string << "#{number.to_s} "


    ❯ 57 string << line.to_s


    ❯ 58 string << "\e[0m"


    ❯ 59 string


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  181. >
    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    52 "#{number.to_s}#{line}"


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  182. # Parses the most indented lines into blocks that are marked


    # and added to the frontier


    def visit_new_blocks


    max_indent = frontier.next_indent_line&.indent


    while (line = frontier.next_indent_line) && (line.indent == max_indent)


    @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|


    record(block: block, name: "add")


    block.mark_invisible if block.valid?


    push(block, name: "add")


    end


    end


    end



    code_search.rb

    View Slide

  183. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  184. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  185. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  186. >
    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    ❯ 52 "#{number.to_s}#{line}"


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  187. >
    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  188. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  189. code_search.rb
    sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    View Slide



  190. end


    end


    # Given an already existing block in the frontier, expand it to see


    # if it contains our invalid syntax


    def expand_invalid_block


    block = frontier.pop


    return unless block


    record(block: block, name: "pop")


    block = @block_expand.call(block)


    push(block, name: "expand")


    end


    def sweep_heredocs





    code_search.rb

    View Slide



  191. end


    end


    # Given an already existing block in the frontier, expand it to see


    # if it contains our invalid syntax


    def expand_invalid_block


    block = frontier.pop


    return unless block


    record(block: block, name: "pop")


    block = @block_expand.call(block)


    push(block, name: "expand")


    end


    def sweep_heredocs





    code_search.rb

    View Slide



  192. end


    end


    # Given an already existing block in the frontier, expand it to see


    # if it contains our invalid syntax


    def expand_invalid_block


    block = frontier.pop


    return unless block


    record(block: block, name: "pop")


    block = @block_expand.call(block)


    push(block, name: "expand")


    end


    def sweep_heredocs





    code_search.rb

    View Slide

  193. # frozen_string_literal: true


    module DeadEnd


    # This class is responsible for taking a code block that exists


    # at a far indentaion and then iteratively increasing the block


    # so that it captures everything within the same indentation block.


    #


    # def dog


    # puts "bow"


    # puts "wow"


    # end


    #


    # block = BlockExpand.new(code_lines: code_lines)


    # .call(CodeBlock.new(lines: code_lines[1]))


    #


    # puts block.to_s


    # # => puts "bow"






    block_expand.rb

    View Slide

  194. # puts "wow"


    # end


    #


    class BlockExpand


    def initialize(code_lines: )


    @code_lines = code_lines


    end


    def call(block)


    if (next_block = expand_neighbors(block, grab_empty: true))


    return next_block


    end


    expand_indent(block)


    end


    def expand_indent(block)



    block_expand.rb

    View Slide

  195. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  196. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  197. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  198. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  199. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  200. >
    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    51 if line.empty?


    ❯ 52 "#{number.to_s}#{line}"


    53 else


    60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  201. >


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    ❯ 51 if line.empty?


    ❯ 53 else


    ❯ 60 end


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  202. >


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end


    46


    47 def code_with_lines


    48 @code_lines.map do |line|


    49 next if line.hidden?


    50 number = line.line_number.to_s.rjust(@digit_count)


    61 end.join


    62 end


    63 end


    64 end


    View Slide

  203. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  204. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  205. def expand_indent(block)


    block = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_adjacent_indent


    .code_block


    end


    def expand_neighbors(block, grab_empty: true)


    scan = AroundBlockScan.new(code_lines: @code_lines, block: block)


    .skip(:hidden?)


    .stop_after_kw


    .scan_neighbors


    # Slurp up empties





    block_expand.rb

    View Slide

  206. code_line.rb


    class CodeLine


    TRAILING_SLASH = ("\\" + $/).freeze


    attr_reader :line, :index, :indent, :original_line


    def initialize(line: , index:)


    @original_line = line.freeze


    @line = @original_line


    if line.strip.empty?


    @empty = true


    @indent = 0


    else


    @empty = false


    @indent = SpaceCount.indent(line)


    end


    @index = index


    @status = nil # valid, invalid, unknown




    View Slide

  207. private def lex_detect!


    lex_array = LexAll.new(source: line)


    kw_count = 0


    end_count = 0


    lex_array.each_with_index do |lex, index|


    next unless lex.type == :on_kw


    case lex.token


    when 'if', 'unless', 'while', 'until'


    # Only count if/unless when it's not a "trailing" if/unless


    kw_count += 1 if !lex.expr_label?


    when 'def', 'case', 'for', 'begin', 'class', 'module', 'do'


    kw_count += 1


    when 'end'


    end_count += 1


    end




    code_line.rb

    View Slide

  208. >
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code)

    View Slide

  209. >
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code)
    [[[1, 0], :on_ignored_nl, "\n", BEG],


    [[2, 0], :on_ident, "string", CMDARG],


    [[2, 6], :on_sp, " ", CMDARG],


    [[2, 7], :on_op, "=", BEG],


    [[2, 8], :on_sp, " ", BEG],


    [[2, 9], :on_heredoc_beg, "<<~EOM", BEG],


    [[2, 15], :on_nl, "\n", BEG],


    [[3, 0], :on_tstring_content,


    " \"I am a here doc\" if true\n",


    BEG],


    [[4, 0], :on_tstring_content, "EOM \n",


    BEG]]

    View Slide

  210. >
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code)
    [[[1, 0], :on_ignored_nl, "\n", BEG],


    [[2, 0], :on_ident, "string", CMDARG],


    [[2, 6], :on_sp, " ", CMDARG],


    [[2, 7], :on_op, "=", BEG],


    [[2, 8], :on_sp, " ", BEG],


    [[2, 9], :on_heredoc_beg, "<<~EOM", BEG],


    [[2, 15], :on_nl, "\n", BEG],


    [[3, 0], :on_tstring_content,


    " \"I am a here doc\" if true\n",


    BEG],


    [[4, 0], :on_tstring_content, "EOM \n",


    BEG]]

    View Slide

  211. >
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code)
    [[[1, 0], :on_ignored_nl, "\n", BEG],


    [[2, 0], :on_ident, "string", CMDARG],


    [[2, 6], :on_sp, " ", CMDARG],


    [[2, 7], :on_op, "=", BEG],


    [[2, 8], :on_sp, " ", BEG],


    [[2, 9], :on_heredoc_beg, "<<~EOM", BEG],


    [[2, 15], :on_nl, "\n", BEG],


    [[3, 0], :on_tstring_content,


    " \"I am a here doc\" if true\n",


    BEG],


    [[4, 0], :on_tstring_content, "EOM \n",


    BEG]]

    View Slide

  212. >
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code.lines[2])

    View Slide

  213. >
    [[[1, 0], :on_sp, " ", BEG],


    [[1, 4], :on_tstring_beg, "\"", BEG],


    [[1, 5], :on_tstring_content,


    "I am a here doc", BEG],


    [[1, 20], :on_tstring_end, "\"", END],


    [[1, 21], :on_sp, " ", END],


    [[1, 22], :on_kw, "if", BEG|LABEL],


    [[1, 24], :on_sp, " ", BEG|LABEL],


    [[1, 25], :on_kw, "true", END],


    [[1, 29], :on_nl, "\n", BEG]]
    source_code = %q{


    string = <<~EOM


    "I am a here doc" if true


    EOM


    }


    Ripper.lex(source_code.lines[2])

    View Slide

  214. private def lex_detect!


    lex_array = LexAll.new(source: line)


    kw_count = 0


    end_count = 0


    lex_array.each_with_index do |lex, index|


    next unless lex.type == :on_kw


    case lex.token


    when 'if', 'unless', 'while', 'until'


    # Only count if/unless when it's not a "trailing" if/unless


    kw_count += 1 if !lex.expr_label?


    when 'def', 'case', 'for', 'begin', 'class', 'module', 'do'


    kw_count += 1


    when 'end'


    end_count += 1


    end




    code_line.rb

    View Slide

  215. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  216. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  217. >
    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    17 def call


    ❯ 18 @io.puts <<~EOM


    ❯ 19


    ❯ 20 DeadEnd: A syntax error was detected


    ❯ 21


    ❯ 22 This code has an unmatched `end` this is caused by either


    ❯ 23 missing a syntax keyword (`def`, `do`, etc.) or inclusion


    ❯ 24 of an extra `end` line:


    ❯ 25 EOM


    26


    27 @io.puts(<<~EOM) if filename


    28 file: #{filename}


    29 EOM


    30


    31 @io.puts <<~EOM



    View Slide

  218. >
    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    17 def call


    26


    27 @io.puts(<<~EOM) if filename


    28 file: #{filename}


    29 EOM


    30


    31 @io.puts <<~EOM


    32 #{code_with_filename}


    33 EOM


    34 end


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")




    View Slide

  219. >
    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    17 def call


    26


    ❯ 27 @io.puts(<<~EOM) if filename


    ❯ 28 file: #{filename}


    ❯ 29 EOM


    30


    31 @io.puts <<~EOM


    32 #{code_with_filename}


    33 EOM


    34 end


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")




    View Slide

  220. >
    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    17 def call


    26


    30


    ❯ 31 @io.puts <<~EOM


    ❯ 32 #{code_with_filename}


    ❯ 33 EOM


    34 end


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines




    View Slide

  221. >
    14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}


    15 end


    16


    17 def call


    26


    30


    34 end


    35


    36 def filename


    37


    38 def code_with_filename


    39 string = String.new("")


    40 string << "```\n"


    41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename


    42 string << code_with_lines


    43 string << "```\n"


    44 string


    45 end




    View Slide

  222. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  223. $ dead_end bad.rb --record tmp/

    View Slide

  224. View Slide

  225. View Slide

  226. sweep(block: block, name: "comments")


    end


    # Main search loop


    def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end



    code_search.rb

    View Slide

  227. def call


    sweep_heredocs


    sweep_comments


    until frontier.holds_all_syntax_errors?


    @tick += 1


    if frontier.expand?


    expand_invalid_block


    else


    visit_new_blocks


    end


    end


    @invalid_blocks.concat(frontier.detect_invalid_blocks )


    @invalid_blocks.sort_by! {|block| block.starts_at }


    self


    end




    code_search.rb

    View Slide

  228. # Example:


    #


    # combination([:a, :b, :c, :d])


    # # => [[:a], [:b], [:c], [:d],


    # [:a, :b], [:a, :c], [:a, :d],


    # [:b, :c], [:b, :d], [:c, :d],


    # [:a, :b, :c], [:a, :b, :d], [:a, :c, :d],


    # [:b, :c, :d],


    # [:a, :b, :c, :d]]


    def self.combination(array)


    guesses = []


    1.upto(array.length).each do |size|


    guesses.concat(array.combination(size).to_a)


    end


    guesses


    end



    code_frontier.rb

    View Slide

  229. # Example:


    #


    # combination([:a, :b, :c, :d])


    # # => [[:a], [:b], [:c], [:d],


    # [:a, :b], [:a, :c], [:a, :d],


    # [:b, :c], [:b, :d], [:c, :d],


    # [:a, :b, :c], [:a, :b, :d], [:a, :c, :d],


    # [:b, :c, :d],


    # [:a, :b, :c, :d]]


    def self.combination(array)


    guesses = []


    1.upto(array.length).each do |size|


    guesses.concat(array.combination(size).to_a)


    end


    guesses


    end



    code_frontier.rb

    View Slide



  230. # 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.select(&:invalid?)).detect do |block_array|


    holds_all_syntax_errors?(block_array)


    end || []


    end


    end


    end


    code_frontier.rb

    View Slide

  231. guesses


    end


    # 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.select(&:invalid?)).detect do |block_array|


    holds_all_syntax_errors?(block_array)


    end || []


    end


    end


    end


    code_frontier.rb

    View Slide

  232. Timeout.timeout(timeout) do


    record_dir ||= ENV["DEBUG"] ? "tmp" : nil


    search = CodeSearch.new(source, record_dir: record_dir).call


    end


    blocks = search.invalid_blocks


    DisplayInvalidBlocks.new(


    blocks: blocks,


    filename: filename,


    terminal: terminal,


    code_lines: search.code_lines,


    invalid_obj: invalid_type(source),


    io: io


    ).call


    dead_end/internals.rb

    View Slide

  233. Timeout.timeout(timeout) do


    record_dir ||= ENV["DEBUG"] ? "tmp" : nil


    search = CodeSearch.new(source, record_dir: record_dir).call


    end


    blocks = search.invalid_blocks


    DisplayInvalidBlocks.new(


    blocks: blocks,


    filename: filename,


    terminal: terminal,


    code_lines: search.code_lines,


    invalid_obj: invalid_type(source),


    io: io


    ).call


    dead_end/internals.rb

    View Slide

  234. Timeout.timeout(timeout) do


    record_dir ||= ENV["DEBUG"] ? "tmp" : nil


    search = CodeSearch.new(source, record_dir: record_dir).call


    end


    blocks = search.invalid_blocks


    DisplayInvalidBlocks.new(


    blocks: blocks,


    filename: filename,


    terminal: terminal,


    code_lines: search.code_lines,


    invalid_obj: invalid_type(source),


    io: io


    ).call


    dead_end/internals.rb

    View Slide

  235. >
    DeadEnd: Missing `end` detected


    This code has a missing `end`. Ensure that all


    syntax keywords (`def`, `do`, etc.) have a matching `end`.


    file: /Users/rschneeman/Documents/projects/dead_end/spec/fixtures/this_project_extr


    simplified:


    1 module SyntaxErrorSearch


    3 class DisplayInvalidBlocks


    4 attr_reader :filename


    ❯ 36 def filename


    ❯ 38 def code_with_filename


    ❯ 45 end


    63 end


    64 end

    View Slide

  236. dead_end

    View Slide

  237. Are you a
    dev?

    View Slide

  238. View Slide

  239. View Slide

  240. Are you an
    open source
    dev?

    View Slide

  241. View Slide

  242. Are you a
    Ruby
    core dev?

    View Slide


  243. 🙏 🙏

    View Slide

  244. Lexing

    Parsing

    Syntax

    SyntaxErrors

    AI

    Pathfinding and goal seeking

    View Slide

  245. View Slide

  246. View Slide

  247. View Slide

  248. View Slide

  249. @schneems

    View Slide

  250. View Slide

  251. Questions?

    View Slide