Class: Ibex::Frontend::DiagnosticRecovery

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/frontend/diagnostic_recovery.rb,
sig/ibex/frontend/diagnostic_recovery.rbs

Overview

Re-runs the generated parser after suppressing only conservative source regions.

Constant Summary collapse

DECLARATION_STARTS =

Returns:

  • (Array[String])
%w[
  pragma include import token prechigh preclow options expect expect_rr start recover on_error_reduce test lexer
  convert display type param printer
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(tokens, mode:, max_diagnostics:) ⇒ DiagnosticRecovery

Returns a new instance of DiagnosticRecovery.

RBS:

  • (Array[Token] tokens, mode: Symbol, max_diagnostics: Integer) -> void

Parameters:

  • tokens (Array[Token])
  • mode: (Symbol)
  • max_diagnostics: (Integer)


18
19
20
21
22
23
24
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 18

def initialize(tokens, mode:, max_diagnostics:)
  @tokens = tokens
  @mode = mode
  @max_diagnostics = max_diagnostics
  @diagnostics = [] #: Array[Diagnostic]
  @suppressed = {} #: Hash[Integer, bool]
end

Instance Method Details

#alternative_range(failure, start, finish) ⇒ Range[Integer]?

RBS:

  • (Integer failure, Integer start, Integer finish) -> Range[Integer]?

Parameters:

  • failure (Integer)
  • start (Integer)
  • finish (Integer)

Returns:

  • (Range[Integer], nil)


199
200
201
202
203
204
205
206
207
208
209
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 199

def alternative_range(failure, start, finish)
  colon = ((start + 1)...finish).find { |index| @tokens[index]&.type == :":" }
  return unless colon

  pipes = top_level_pipes(colon + 1, finish)
  return if pipes.empty?

  previous = pipes.reverse.find { |index| index < failure }
  following = pipes.find { |index| index >= failure }
  alternative_slice(previous, following, colon, finish)
end

#alternative_slice(previous, following, colon, finish) ⇒ Range[Integer]?

RBS:

  • (Integer? previous, Integer? following, Integer colon, Integer finish) -> Range[Integer]?

Parameters:

  • previous (Integer, nil)
  • following (Integer, nil)
  • colon (Integer)
  • finish (Integer)

Returns:

  • (Range[Integer], nil)


212
213
214
215
216
217
218
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 212

def alternative_slice(previous, following, colon, finish)
  if previous
    following ? (previous...following) : (previous...finish)
  elsif following
    (colon + 1)..following
  end
end

#append_diagnostic(diagnostic) ⇒ void

This method returns an undefined value.

RBS:

  • (Diagnostic diagnostic) -> void

Parameters:



61
62
63
64
65
66
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 61

def append_diagnostic(diagnostic)
  key = diagnostic_key(diagnostic)
  return if @diagnostics.any? { |existing| diagnostic_key(existing) == key }

  @diagnostics << diagnostic
end

#declaration_finish(start, rule_marker, starts) ⇒ Integer

RBS:

  • (Integer start, Integer rule_marker, Array[Integer] starts) -> Integer

Parameters:

  • start (Integer)
  • rule_marker (Integer)
  • starts (Array[Integer])

Returns:

  • (Integer)


134
135
136
137
138
139
140
141
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 134

def declaration_finish(start, rule_marker, starts)
  value = @tokens.fetch(start).value
  return matching_declaration_end(start, rule_marker, value == "prechigh" ? "preclow" : "prechigh") if
    %w[prechigh preclow].include?(value)
  return matching_declaration_end(start, rule_marker, "end") if %w[convert lexer].include?(value)

  starts.find { |index| index > start } || rule_marker
end

#declaration_range(failure, rule_marker) ⇒ Range[Integer]?

RBS:

  • (Integer failure, Integer rule_marker) -> Range[Integer]?

Parameters:

  • failure (Integer)
  • rule_marker (Integer)

Returns:

  • (Range[Integer], nil)


118
119
120
121
122
123
124
125
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 118

def declaration_range(failure, rule_marker)
  starts = (0...rule_marker).select { |index| declaration_start?(index) }
  start = starts.reverse.find { |index| index <= failure }
  return unless start

  finish = declaration_finish(start, rule_marker, starts)
  start...finish
end

#declaration_start?(index) ⇒ Boolean

RBS:

  • (Integer index) -> bool

Parameters:

  • index (Integer)

Returns:

  • (Boolean)


128
129
130
131
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 128

def declaration_start?(index)
  token = @tokens[index]
  token&.type == :identifier && DECLARATION_STARTS.include?(token.value)
end

#diagnostic_from(error, token, phase:) ⇒ Diagnostic

RBS:

  • (Exception error, Token? token, phase: Symbol) -> Diagnostic

Parameters:

  • error (Exception)
  • token (Token, nil)
  • phase: (Symbol)

Returns:



245
246
247
248
249
250
251
252
253
254
255
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 245

def diagnostic_from(error, token, phase:)
  matching = @tokens.find { |candidate| error.message.start_with?("#{candidate.location}:") }
  token = matching || token || @tokens.last
  location = token&.location || Location.new(file: "(grammar)", line: 1, column: 1)
  prefix = "#{location}: "
  message = error.message.delete_prefix(prefix)
  expected, received = expected_and_received(message)
  Diagnostic.new(code: "frontend.#{phase}_error", phase: phase, message: message,
                 location: location, span: token&.span, expected: expected,
                 received: received, rendered: error.message)
end

#diagnostic_key(diagnostic) ⇒ [ String, String, Integer, Integer, String ]

RBS:

  • (Diagnostic diagnostic) -> [String, String, Integer, Integer, String]

Parameters:

Returns:

  • ([ String, String, Integer, Integer, String ])


69
70
71
72
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 69

def diagnostic_key(diagnostic)
  location = diagnostic.location
  [diagnostic.phase.to_s, location.file, location.line, location.column, diagnostic.message]
end

#diagnostic_sort_key(diagnostic) ⇒ [ String, Integer, Integer, String, String ]

RBS:

  • (Diagnostic diagnostic) -> [String, Integer, Integer, String, String]

Parameters:

Returns:

  • ([ String, Integer, Integer, String, String ])


83
84
85
86
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 83

def diagnostic_sort_key(diagnostic)
  location = diagnostic.location
  [location.file, location.line, location.column, diagnostic.phase.to_s, diagnostic.code]
end

#expected_and_received(message) ⇒ [ Array[String], String? ]

RBS:

  • (String message) -> [Array[String], String?]

Parameters:

  • message (String)

Returns:

  • ([ Array[String], String? ])


258
259
260
261
262
263
264
265
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 258

def expected_and_received(message)
  match = message.match(/\Aexpected (.+), got (.+)\z/)
  return [[], nil] unless match

  expected = match[1]
  received = match[2]
  [expected ? [expected] : [], received]
end

#grammar_end_index(rule_marker) ⇒ Integer

RBS:

  • (Integer rule_marker) -> Integer

Parameters:

  • rule_marker (Integer)

Returns:

  • (Integer)


161
162
163
164
165
166
167
168
169
170
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 161

def grammar_end_index(rule_marker)
  depth = 0
  ((rule_marker + 1)...@tokens.length).each do |index|
    token = @tokens.fetch(index)
    depth += 1 if token.type == :"("
    depth -= 1 if token.type == :")" && depth.positive?
    return index if depth.zero? && token.type == :identifier && token.value == "end"
  end
  @tokens.length - 1
end

#lhs_candidate?(index, lhs_column) ⇒ Boolean

RBS:

  • (Integer index, Integer? lhs_column) -> bool

Parameters:

  • index (Integer)
  • lhs_column (Integer, nil)

Returns:

  • (Boolean)


190
191
192
193
194
195
196
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 190

def lhs_candidate?(index, lhs_column)
  token = @tokens[index]
  following = @tokens[index + 1]
  return false unless token&.type == :identifier && following&.type == :":"

  lhs_column.nil? || token.location.column <= lhs_column
end

#matching_declaration_end(start, fallback, closer) ⇒ Integer

RBS:

  • (Integer start, Integer fallback, String closer) -> Integer

Parameters:

  • start (Integer)
  • fallback (Integer)
  • closer (String)

Returns:

  • (Integer)


144
145
146
147
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 144

def matching_declaration_end(start, fallback, closer)
  index = ((start + 1)...fallback).find { |candidate| @tokens[candidate]&.value == closer }
  index ? index + 1 : fallback
end

#parse[ AST::Root | AST::Fragment | nil, Array[Diagnostic] ]

RBS:

  • () -> [AST::Root | AST::Fragment | nil, Array[Diagnostic]]

Returns:



27
28
29
30
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 27

def parse
  ast = parse_until_stable
  [ast, sorted_diagnostics]
end

#parse_until_stableAST::Root, ...

RBS:

  • () -> (AST::Root | AST::Fragment | nil)

Returns:



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 35

def parse_until_stable
  attempts = 0
  loop do
    parser = GeneratedParser.new(working_tokens, mode: @mode)
    begin
      return parser.parse
    rescue Ibex::Error => e
      return if @diagnostics.length >= @max_diagnostics

      diagnostic = diagnostic_from(e, parser.diagnostic_token, phase: :syntax)
      append_diagnostic(diagnostic)
      range = suppression_range(parser.diagnostic_token, e)
      return unless range && suppress(range)

      attempts += 1
      return if attempts >= @tokens.length
    end
  end
end

#rule_definition_starts(rule_marker, grammar_end) ⇒ Array[Integer]

RBS:

  • (Integer rule_marker, Integer grammar_end) -> Array[Integer]

Parameters:

  • rule_marker (Integer)
  • grammar_end (Integer)

Returns:

  • (Array[Integer])


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 173

def rule_definition_starts(rule_marker, grammar_end)
  starts = [] #: Array[Integer]
  depth = 0
  lhs_column = nil #: Integer?
  ((rule_marker + 1)...grammar_end).each do |index|
    token = @tokens.fetch(index)
    depth += 1 if token.type == :"("
    depth -= 1 if token.type == :")" && depth.positive?
    next unless depth.zero? && lhs_candidate?(index, lhs_column)

    lhs_column ||= token.location.column
    starts << index
  end
  starts
end

#rule_marker_indexInteger?

RBS:

  • () -> Integer?

Returns:

  • (Integer, nil)


113
114
115
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 113

def rule_marker_index
  @tokens.index { |token| token.type == :identifier && token.value == "rule" }
end

#rule_range(failure, rule_marker) ⇒ Range[Integer]?

RBS:

  • (Integer failure, Integer rule_marker) -> Range[Integer]?

Parameters:

  • failure (Integer)
  • rule_marker (Integer)

Returns:

  • (Range[Integer], nil)


150
151
152
153
154
155
156
157
158
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 150

def rule_range(failure, rule_marker)
  grammar_end = grammar_end_index(rule_marker)
  definitions = rule_definition_starts(rule_marker, grammar_end)
  start = definitions.reverse.find { |index| index <= failure }
  return unless start

  finish = definitions.find { |index| index > start } || grammar_end
  alternative_range(failure, start, finish) || (start...finish)
end

#sorted_diagnosticsArray[Diagnostic]

RBS:

  • () -> Array[Diagnostic]

Returns:



75
76
77
78
79
80
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 75

def sorted_diagnostics
  @diagnostics.uniq { |diagnostic| diagnostic_key(diagnostic) }
              .sort_by { |diagnostic| diagnostic_sort_key(diagnostic) }
              .first(@max_diagnostics)
              .freeze
end

#suppress(range) ⇒ Boolean

RBS:

  • (Range[Integer] range) -> bool

Parameters:

  • range (Range[Integer])

Returns:

  • (Boolean)


232
233
234
235
236
237
238
239
240
241
242
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 232

def suppress(range)
  changed = false
  range.each do |index|
    next if index.negative? || index >= @tokens.length || @tokens[index]&.type == :eof
    next if @suppressed[index]

    @suppressed[index] = true
    changed = true
  end
  changed
end

#suppression_range(token, error) ⇒ Range[Integer]?

RBS:

  • (Token? token, Exception error) -> Range[Integer]?

Parameters:

  • token (Token, nil)
  • error (Exception)

Returns:

  • (Range[Integer], nil)


89
90
91
92
93
94
95
96
97
98
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 89

def suppression_range(token, error)
  index = token_index(token) || token_index_for_message(error.message)
  return unless index

  rule_marker = rule_marker_index
  return declaration_range(index, rule_marker) if rule_marker && index < rule_marker
  return rule_range(index, rule_marker) if rule_marker

  nil
end

#token_index(token) ⇒ Integer?

RBS:

  • (Token? token) -> Integer?

Parameters:

Returns:

  • (Integer, nil)


101
102
103
104
105
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 101

def token_index(token)
  return unless token

  @tokens.index { |candidate| candidate.equal?(token) }
end

#token_index_for_message(message) ⇒ Integer?

RBS:

  • (String message) -> Integer?

Parameters:

  • message (String)

Returns:

  • (Integer, nil)


108
109
110
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 108

def token_index_for_message(message)
  @tokens.index { |token| message.start_with?("#{token.location}:") }
end

#top_level_pipes(start, finish) ⇒ Array[Integer]

RBS:

  • (Integer start, Integer finish) -> Array[Integer]

Parameters:

  • start (Integer)
  • finish (Integer)

Returns:

  • (Array[Integer])


221
222
223
224
225
226
227
228
229
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 221

def top_level_pipes(start, finish)
  depth = 0
  (start...finish).filter_map do |index|
    token = @tokens.fetch(index)
    depth += 1 if token.type == :"("
    depth -= 1 if token.type == :")" && depth.positive?
    index if depth.zero? && token.type == :|
  end
end

#working_tokensArray[Token]

RBS:

  • () -> Array[Token]

Returns:



56
57
58
# File 'lib/ibex/frontend/diagnostic_recovery.rb', line 56

def working_tokens
  @tokens.each_with_index.filter_map { |token, index| token unless @suppressed[index] }
end