flexr 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +74 -28
  3. data/benchmark/golden/calculator_lexer.sha256 +1 -1
  4. data/benchmark/golden/json_lexer.sha256 +1 -1
  5. data/benchmark/golden/regexp_tokenizer.sha256 +1 -1
  6. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -1
  7. data/benchmark/golden/toy_lang_lexer.sha256 +1 -1
  8. data/benchmark/golden/with_lrama_lexer.sha256 +1 -1
  9. data/benchmark/golden/with_racc_lexer.sha256 +1 -1
  10. data/docs/README.md +2 -1
  11. data/docs/explanation/backends.md +13 -10
  12. data/docs/explanation/matching-semantics.md +4 -2
  13. data/docs/explanation/security-model.md +10 -3
  14. data/docs/explanation/unicode-and-encoding.md +3 -1
  15. data/docs/how-to/deploy-a-standalone-lexer.md +5 -3
  16. data/docs/how-to/generate-a-lexer.md +3 -3
  17. data/docs/how-to/handle-errors.md +4 -1
  18. data/docs/how-to/run-a-lexer-at-runtime.md +5 -0
  19. data/docs/how-to/track-token-locations.md +2 -1
  20. data/docs/how-to/tune-performance.md +3 -3
  21. data/docs/perf-log.md +16 -0
  22. data/docs/reference/actions.md +5 -2
  23. data/docs/reference/diagnostics.md +8 -0
  24. data/docs/reference/dsl.md +19 -5
  25. data/docs/reference/errors.md +6 -3
  26. data/docs/reference/generated-artifacts.md +29 -2
  27. data/docs/reference/public-api.md +7 -2
  28. data/docs/reference/regexp.md +8 -6
  29. data/docs/reference/runtime.md +20 -3
  30. data/docs/reference/tokens-and-locations.md +6 -4
  31. data/docs/releases/v1.1.0.md +44 -0
  32. data/lib/flexr/action_resolver.rb +15 -0
  33. data/lib/flexr/artifact_writer.rb +61 -0
  34. data/lib/flexr/automaton/accel.rb +9 -4
  35. data/lib/flexr/automaton/analysis.rb +47 -6
  36. data/lib/flexr/automaton/backend_cost_model.rb +40 -0
  37. data/lib/flexr/automaton/compiler.rb +50 -71
  38. data/lib/flexr/automaton/dfa.rb +106 -19
  39. data/lib/flexr/automaton/minimizer.rb +79 -31
  40. data/lib/flexr/automaton/nfa.rb +45 -13
  41. data/lib/flexr/automaton/types.rb +14 -0
  42. data/lib/flexr/cli.rb +11 -5
  43. data/lib/flexr/codegen/direct.rb +5 -41
  44. data/lib/flexr/codegen/table.rb +175 -69
  45. data/lib/flexr/codegen.rb +8 -0
  46. data/lib/flexr/configuration.rb +37 -0
  47. data/lib/flexr/dsl.rb +105 -39
  48. data/lib/flexr/errors.rb +1 -0
  49. data/lib/flexr/generated.rb +61 -35
  50. data/lib/flexr/generator.rb +47 -82
  51. data/lib/flexr/importer.rb +4 -40
  52. data/lib/flexr/options.rb +5 -3
  53. data/lib/flexr/rake_task.rb +6 -1
  54. data/lib/flexr/regexp/ast.rb +5 -2
  55. data/lib/flexr/regexp/normalizer.rb +62 -16
  56. data/lib/flexr/regexp/parser.rb +121 -46
  57. data/lib/flexr/regexp/tokenizer.rb +192 -67
  58. data/lib/flexr/runtime/buffer.rb +115 -9
  59. data/lib/flexr/runtime/core.rb +214 -47
  60. data/lib/flexr/runtime/errors.rb +64 -4
  61. data/lib/flexr/runtime/interpreter.rb +178 -71
  62. data/lib/flexr/runtime.rb +70 -0
  63. data/lib/flexr/source/passthrough.rb +62 -9
  64. data/lib/flexr/source/prism_reader.rb +175 -46
  65. data/lib/flexr/source/static_eval.rb +40 -4
  66. data/lib/flexr/source.rb +6 -0
  67. data/lib/flexr/unicode/data/properties.rb +1 -1
  68. data/lib/flexr/unicode/data.rb +11 -0
  69. data/lib/flexr/unicode/property.rb +3 -5
  70. data/lib/flexr/unicode/reference_regexp.rb +4 -0
  71. data/lib/flexr/unicode/version.rb +7 -0
  72. data/lib/flexr/version.rb +1 -1
  73. data/lib/flexr.rb +9 -76
  74. data/tools/coverage.rb +6 -1
  75. metadata +12 -1
@@ -3,20 +3,45 @@
3
3
  module Flexr
4
4
  module Runtime
5
5
  def initialize(input, filename: nil, error_mode: :raise, max_token_size: 16 * 1024 * 1024,
6
- max_state_stack: 1024, chunk_size: Runtime::Buffer::DEFAULT_CHUNK_SIZE)
6
+ max_lookahead_size: nil, max_buffer_size: 64 * 1024 * 1024,
7
+ max_state_stack: 1024, max_steps: nil, cancellation: nil, retain_input: true,
8
+ chunk_size: Runtime::Buffer::DEFAULT_CHUNK_SIZE)
7
9
  raise ArgumentError, "max_token_size must be non-negative" if max_token_size.to_i.negative?
10
+ raise ArgumentError, "max_lookahead_size must be non-negative" if max_lookahead_size&.to_i&.negative?
11
+ raise ArgumentError, "max_buffer_size must be non-negative" if max_buffer_size.to_i.negative?
8
12
  raise ArgumentError, "max_state_stack must be non-negative" if max_state_stack.to_i.negative?
13
+ raise ArgumentError, "max_steps must be non-negative" if max_steps&.to_i&.negative?
14
+ raise ArgumentError, "cancellation must respond to call" if cancellation && !cancellation.respond_to?(:call)
9
15
 
10
16
  self.class.compile!
11
- @buffer = Runtime::Buffer.new(input, chunk_size: chunk_size)
12
- @input = input.is_a?(String) ? input : nil
13
- @line_tracking_needed = !@input || input.include?("\n")
17
+ config = self.class.__flexr_config
18
+ @buffer = Runtime::Buffer.new(
19
+ input, chunk_size: chunk_size, max_buffer_size: max_buffer_size,
20
+ retain_input: retain_input, filename: filename
21
+ )
22
+ @string_input = input.is_a?(String)
23
+ @stable_input_end = input.bytesize if @string_input && input.frozen?
24
+ @valid_utf8_input = @string_input && input.frozen? && (config.encoding != Encoding::UTF_8 || begin
25
+ bytes = input.encoding == Encoding::UTF_8 ? input : input.dup.force_encoding(Encoding::UTF_8)
26
+ bytes.valid_encoding?
27
+ end)
28
+ @simple_location_input = @string_input && input.frozen? && retain_input &&
29
+ input.ascii_only? && !input.include?("\n")
30
+ @retain_input = retain_input
14
31
  @filename = filename
15
32
  @error_mode = error_mode
33
+ @token_kind = config.token_kind
34
+ @accel_mode = config.options.fetch(:accel, :auto)
16
35
  @max_token_size = max_token_size.to_i
36
+ @token_limit_required = !@string_input || !input.frozen? || input.bytesize > @max_token_size
37
+ @max_lookahead_size = (max_lookahead_size || max_token_size).to_i
17
38
  @max_state_stack = max_state_stack.to_i
39
+ @max_steps = max_steps&.to_i
40
+ @cancellation = cancellation
41
+ @steps = 0
18
42
  @position = 0
19
43
  @line = 1
44
+ @column = 1
20
45
  @state = :initial
21
46
  @state_stack = []
22
47
  @pending = nil
@@ -25,18 +50,22 @@ module Flexr
25
50
  @match_end = 0
26
51
  @text_start = 0
27
52
  @text_line = 1
53
+ @text_column = 1
28
54
  @bol = true
29
55
  @more_start = nil
30
56
  @more_line = nil
57
+ @more_column = nil
31
58
  @more_requested = false
32
- @candidate_token_size = 0
59
+ @non_progress_signatures = {}
60
+ @active_rule = nil
33
61
  @eof_fired_states = {}
34
62
  @on_error = nil
35
63
  @halted = false
36
64
  @interpreter = nil
65
+ @generated_scanner = generated_runtime? && respond_to?(:scan_one, true)
37
66
  end
38
67
 
39
- attr_reader :filename, :error_mode, :buffer, :max_token_size
68
+ attr_reader :filename, :error_mode, :buffer, :max_token_size, :max_lookahead_size, :steps
40
69
 
41
70
  attr_writer :on_error
42
71
 
@@ -49,6 +78,7 @@ module Flexr
49
78
 
50
79
  loop do
51
80
  return nil if @halted
81
+ consume_step! if scan_steps_guarded?
52
82
 
53
83
  if eof? && @pending.nil?
54
84
  eof_action = self.class.__flexr_spec.eof_rules[@state]
@@ -58,6 +88,7 @@ module Flexr
58
88
  @match_end = @position
59
89
  @text_start = @position
60
90
  @text_line = @line
91
+ @text_column = @column
61
92
  @matched = nil
62
93
  instance_exec(&eof_action)
63
94
  token = @pending
@@ -68,7 +99,8 @@ module Flexr
68
99
  return nil
69
100
  end
70
101
 
71
- match = if generated_runtime? && respond_to?(:scan_one, true)
102
+ @active_rule = nil
103
+ match = if @generated_scanner
72
104
  scan_one
73
105
  else
74
106
  (@interpreter ||= Runtime::Interpreter.new(self)).scan
@@ -84,22 +116,28 @@ module Flexr
84
116
  end
85
117
  @match_start = match.start_pos
86
118
  @match_end = match.end_pos
119
+ scan_state = @state
87
120
  if @more_start
88
121
  @text_start = @more_start
89
122
  @text_line = @more_line
123
+ @text_column = @more_column
90
124
  else
91
125
  @text_start = @match_start
92
126
  @text_line = @line
127
+ @text_column = @column
93
128
  end
94
129
  @matched = nil
95
130
  @more_requested = false
96
131
  @position = match.end_pos
97
132
  empty_match = match.end_pos == match.start_pos
133
+ @active_rule = match.rule
98
134
  execute(match.rule)
99
135
  finalize_more
100
- force_empty_match_progress! if empty_match && self.class.__flexr_config.options[:allow_empty_match]
101
- ensure_token_size!
136
+ ensure_progress!(match, scan_state, empty_match: empty_match) unless
137
+ @position > match.start_pos && @non_progress_signatures.empty?
138
+ ensure_token_size! if @token_limit_required
102
139
  update_position
140
+ discard_consumed_input! unless @retain_input
103
141
  token = @pending
104
142
  @pending = nil
105
143
  return token if token
@@ -113,7 +151,7 @@ module Flexr
113
151
  token = next_token
114
152
  break unless token
115
153
 
116
- if self.class.__flexr_config.token_kind == :yield
154
+ if @token_kind == :yield
117
155
  yield(*token)
118
156
  else
119
157
  yield token
@@ -153,8 +191,46 @@ module Flexr
153
191
  @more_start
154
192
  end
155
193
 
156
- def defer_token_size_check!(size)
157
- @candidate_token_size = size if size > @candidate_token_size
194
+ def defer_token_size_check!(size, rule: nil)
195
+ return if size <= @max_token_size
196
+
197
+ raise Runtime::TokenTooLargeError.new(
198
+ filename: @filename, byte_pos: @more_start || @position, line: @line,
199
+ rule: rule_index(rule || @active_rule)
200
+ )
201
+ end
202
+
203
+ def ensure_lookahead_size!(size, rule: nil)
204
+ return if size <= @max_lookahead_size
205
+
206
+ raise Runtime::LookaheadTooLargeError.new(
207
+ filename: @filename, byte_pos: @position, line: @line,
208
+ rule: rule_index(rule || @active_rule)
209
+ )
210
+ end
211
+
212
+ def consume_step!(count = 1)
213
+ return unless scan_steps_guarded?
214
+
215
+ @steps += count
216
+ if @max_steps && @steps > @max_steps
217
+ raise Runtime::StepLimitError.new(
218
+ filename: @filename, byte_pos: @position, line: @line, rule: rule_index(@active_rule)
219
+ )
220
+ end
221
+ return unless cancelled?
222
+
223
+ raise Runtime::CancelledError.new(
224
+ filename: @filename, byte_pos: @position, line: @line, rule: rule_index(@active_rule)
225
+ )
226
+ end
227
+
228
+ def scan_steps_guarded?
229
+ !@max_steps.nil? || !@cancellation.nil?
230
+ end
231
+
232
+ def token_limit_required?
233
+ @token_limit_required
158
234
  end
159
235
 
160
236
  def utf8_input?
@@ -162,7 +238,7 @@ module Flexr
162
238
  end
163
239
 
164
240
  def valid_utf8_at?(position)
165
- !utf8_input? || @buffer.valid_utf8_at?(position)
241
+ @valid_utf8_input || !utf8_input? || @buffer.valid_utf8_at?(position)
166
242
  end
167
243
 
168
244
  def utf8_boundary?(position)
@@ -184,11 +260,12 @@ module Flexr
184
260
  end
185
261
 
186
262
  def binary_input
187
- @buffer.source.b
263
+ source = @buffer.source
264
+ source.encoding == ::Encoding::BINARY ? source : source.b
188
265
  end
189
266
 
190
267
  def emit(type, value = text)
191
- @pending = case self.class.__flexr_config.token_kind
268
+ @pending = case @token_kind
192
269
  when :struct
193
270
  Runtime::Token.new(type: type, value: value, location: last_location)
194
271
  else
@@ -201,7 +278,7 @@ module Flexr
201
278
  end
202
279
 
203
280
  def error!(message)
204
- error = LexError.new(message, filename: @filename, byte_pos: @match_start, line: @line, text: text)
281
+ error = LexError.new(message, filename: @filename, byte_pos: @match_start, line: @text_line, text: text)
205
282
  if @on_error
206
283
  action = @on_error.call(error)
207
284
  return @pending = nil if action == :skip
@@ -211,6 +288,11 @@ module Flexr
211
288
  return @pending = nil
212
289
  end
213
290
  return emit(:error, text) if action == :token
291
+
292
+ raise Runtime::InvalidRecoveryActionError.new(
293
+ action: action, filename: @filename, byte_pos: @match_start, line: @text_line,
294
+ text: text, rule: rule_index(@active_rule)
295
+ )
214
296
  end
215
297
  case @error_mode
216
298
  when :token
@@ -239,7 +321,8 @@ module Flexr
239
321
  if @state_stack.length >= @max_state_stack
240
322
  raise Runtime::StateStackOverflowError.new(
241
323
  "state stack exceeds max_state_stack (#{@max_state_stack})",
242
- filename: @filename, byte_pos: @position, line: @line, text: text
324
+ filename: @filename, byte_pos: @position, line: @line, text: text,
325
+ rule: rule_index(@active_rule)
243
326
  )
244
327
  end
245
328
 
@@ -266,7 +349,11 @@ module Flexr
266
349
  matched_bytes = @match_end - @match_start
267
350
  raise ArgumentError, "less must not exceed matched bytes" if count.negative? || count > matched_bytes
268
351
 
269
- @position = @match_start + count
352
+ new_position = @match_start + count
353
+ raise ArgumentError, "less must end at a UTF-8 codepoint boundary" if
354
+ utf8_input? && !@buffer.utf8_boundary?(new_position)
355
+
356
+ @position = new_position
270
357
  @match_end = @position
271
358
  @matched = nil
272
359
  end
@@ -277,10 +364,14 @@ module Flexr
277
364
 
278
365
  def last_location
279
366
  line_begin = @text_line || @line
367
+ column_begin = @text_column || @column
368
+ line_end, column_end = location_after(
369
+ @text_start, @match_end, line: line_begin, column: column_begin
370
+ )
280
371
  Runtime::Location.new(
281
372
  filename: @filename, byte_begin: @text_start, byte_end: @match_end,
282
- line_begin: line_begin, line_end: line_begin + text.to_s.b.count("\n"),
283
- column_values: [column_at(@text_start), column_at(@match_end)],
373
+ line_begin: line_begin, line_end: line_end,
374
+ column_values: [column_begin, column_end],
284
375
  eager_columns: self.class.__flexr_config.options[:eager_columns] == true
285
376
  )
286
377
  end
@@ -304,9 +395,11 @@ module Flexr
304
395
  if @more_requested
305
396
  @more_start = @text_start
306
397
  @more_line = @text_line
398
+ @more_column = @text_column
307
399
  else
308
400
  @more_start = nil
309
401
  @more_line = nil
402
+ @more_column = nil
310
403
  end
311
404
  @more_requested = false
312
405
  end
@@ -316,34 +409,31 @@ module Flexr
316
409
  end
317
410
 
318
411
  def ensure_token_size!
319
- actual_size = @match_end - @text_start
320
- @candidate_token_size = 0
321
- return if actual_size <= @max_token_size
412
+ return unless @token_limit_required
322
413
 
323
- raise Runtime::TokenTooLargeError
414
+ actual_size = @match_end - @text_start
415
+ defer_token_size_check!(actual_size, rule: @active_rule)
324
416
  end
325
417
 
326
418
  def force_empty_match_progress!
327
419
  return if eof?
328
420
 
329
- byte = @buffer.byteslice(@position, 1).to_s.b
330
- @position += 1
331
- @line += 1 if byte == "\n"
332
- @bol = byte == "\n"
421
+ length = utf8_input? ? @buffer.utf8_character_length(@position) : 1
422
+ ending = @position + length
423
+ advance_location!(@position, ending)
424
+ @position = ending
425
+ @bol = @buffer.getbyte(@position - 1) == 0x0a
333
426
  end
334
427
 
335
428
  def update_position
336
- if @match_end > @match_start
337
- length = @match_end - @match_start
338
- @line += if length == 1
339
- @buffer.source.getbyte(@match_start) == 0x0a ? 1 : 0
340
- elsif @line_tracking_needed && (newline = @buffer.source.index("\n", @match_start)) && newline < @match_end
341
- @buffer.byteslice(@match_start, length).to_s.count("\n")
342
- else
343
- 0
344
- end
429
+ if @simple_location_input
430
+ @column += @match_end - @match_start
431
+ @bol = @match_end.zero?
432
+ return
345
433
  end
346
- @bol = @match_end.zero? || @buffer.source.getbyte(@match_end - 1) == 0x0a
434
+
435
+ advance_location!(@match_start, @match_end)
436
+ @bol = @match_end.zero? || @buffer.getbyte(@match_end - 1) == 0x0a
347
437
  end
348
438
 
349
439
  def handle_unmatched_byte
@@ -352,17 +442,21 @@ module Flexr
352
442
  @match_end = @position + 1
353
443
  @text_start = @match_start
354
444
  @text_line = @line
445
+ @text_column = @column
355
446
  @position += 1
356
- @line += 1 if bad.to_s.b == "\n"
447
+ @non_progress_signatures.clear
448
+ advance_location!(@match_start, @match_end)
357
449
  @bol = bad.to_s.b == "\n"
358
450
  error!("unexpected byte #{bad.inspect}")
359
451
  token = @pending
360
452
  @pending = nil
453
+ discard_consumed_input! unless @retain_input
361
454
  token
362
455
  end
363
456
 
364
457
  def eof?
365
- return @position >= @buffer.bytesize if @input
458
+ return @position >= @stable_input_end if @stable_input_end
459
+ return @position >= @buffer.bytesize if @string_input
366
460
 
367
461
  @buffer.eof?(@position)
368
462
  end
@@ -374,15 +468,88 @@ module Flexr
374
468
  raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
375
469
  end
376
470
 
377
- def column_at(position)
378
- prefix = @buffer.byteslice(0...position).to_s
379
- last_newline = prefix.rindex("\n")
380
- line_prefix = prefix.byteslice((last_newline ? last_newline + 1 : 0)..).to_s
381
- if utf8_input? && line_prefix.dup.force_encoding(Encoding::UTF_8).valid_encoding?
382
- line_prefix.force_encoding(Encoding::UTF_8).length + 1
471
+ def ensure_progress!(match, scan_state, empty_match:)
472
+ if @position > match.start_pos
473
+ @non_progress_signatures.clear
474
+ return
475
+ end
476
+ return if @halted
477
+
478
+ if empty_match && @state == scan_state && self.class.__flexr_config.options[:allow_empty_match]
479
+ force_empty_match_progress!
480
+ @non_progress_signatures.clear
481
+ return
482
+ end
483
+
484
+ signature = [match.start_pos, scan_state, match.rule.index]
485
+ repeated = @non_progress_signatures.key?(signature)
486
+ @non_progress_signatures[signature] = true
487
+ return if @state != scan_state && !repeated
488
+
489
+ raise Runtime::NonProgressError.new(
490
+ filename: @filename, byte_pos: match.start_pos, line: @line,
491
+ text: text, rule: match.rule.index
492
+ )
493
+ end
494
+
495
+ def advance_location!(starting, ending)
496
+ return if ending <= starting
497
+
498
+ if @simple_location_input
499
+ @column += ending - starting
500
+ return
501
+ end
502
+
503
+ value = location_value(starting, ending)
504
+ location_bytes = value.valid_encoding? ? value : value.b
505
+ newline_count = location_bytes.count("\n")
506
+ if newline_count.zero?
507
+ @column += display_length(value)
383
508
  else
384
- line_prefix.bytesize + 1
509
+ tail = value.byteslice(((location_bytes.rindex("\n") || -1) + 1)..).to_s
510
+ @line += newline_count
511
+ @column = display_length(tail) + 1
385
512
  end
386
513
  end
514
+
515
+ def location_after(starting, ending, line:, column:)
516
+ return [line, column] if ending <= starting
517
+
518
+ value = location_value(starting, ending)
519
+ location_bytes = value.valid_encoding? ? value : value.b
520
+ newline_count = location_bytes.count("\n")
521
+ return [line, column + display_length(value)] if newline_count.zero?
522
+
523
+ tail = value.byteslice(((location_bytes.rindex("\n") || -1) + 1)..).to_s
524
+ [line + newline_count, display_length(tail) + 1]
525
+ end
526
+
527
+ def display_length(value)
528
+ return value.bytesize unless utf8_input?
529
+ return value.bytesize if value.ascii_only?
530
+
531
+ utf8 = value.encoding == Encoding::UTF_8 ? value : value.dup.force_encoding(Encoding::UTF_8)
532
+ utf8.valid_encoding? ? utf8.length : value.bytesize
533
+ end
534
+
535
+ def location_value(starting, ending)
536
+ return @matched if @matched && starting == @text_start && ending == @match_end
537
+
538
+ @buffer.byteslice(starting...ending).to_s
539
+ end
540
+
541
+ def discard_consumed_input!
542
+ @buffer.discard_before(@more_start || @position)
543
+ end
544
+
545
+ def cancelled?
546
+ return false unless @cancellation
547
+
548
+ @cancellation.arity.zero? ? @cancellation.call : @cancellation.call(self)
549
+ end
550
+
551
+ def rule_index(rule)
552
+ rule.respond_to?(:index) ? rule.index : rule
553
+ end
387
554
  end
388
555
  end
@@ -5,18 +5,78 @@ module Flexr
5
5
  class TokenTooLargeError < LexError
6
6
  CODE = "FLEXR-E012"
7
7
 
8
- attr_reader :code
8
+ attr_reader :code, :rule
9
9
 
10
- def initialize(message = "token exceeds max_token_size", filename: nil, byte_pos: nil, line: nil, text: nil)
10
+ def initialize(message = "token exceeds max_token_size", filename: nil, byte_pos: nil, line: nil, text: nil,
11
+ rule: nil)
11
12
  @code = CODE
13
+ @rule = rule
12
14
  diagnostic = Diagnostics.error(
13
15
  CODE,
14
16
  message,
15
- help: "increase max_token_size or split the input token"
17
+ help: "increase max_token_size or split the input token",
18
+ note: rule.nil? ? nil : "rule #{rule} at byte #{byte_pos}"
16
19
  )
17
20
  super(message, filename: filename, byte_pos: byte_pos, line: line, text: text, diagnostic: diagnostic)
18
21
  end
19
22
  end
20
- class StateStackOverflowError < LexError; end
23
+
24
+ class ResourceLimitError < LexError
25
+ attr_reader :code, :rule
26
+
27
+ def initialize(code, message, help:, filename: nil, byte_pos: nil, line: nil, text: nil, rule: nil)
28
+ @code = code
29
+ @rule = rule
30
+ diagnostic = Diagnostics.error(code, message, help: help, note: rule.nil? ? nil : "rule #{rule}")
31
+ super(message, filename: filename, byte_pos: byte_pos, line: line, text: text, diagnostic: diagnostic)
32
+ end
33
+ end
34
+
35
+ class NonProgressError < ResourceLimitError
36
+ def initialize(**context)
37
+ super("FLEXR-E021", "lexer action did not make progress",
38
+ help: "consume input or change to a state that can consume it", **context)
39
+ end
40
+ end
41
+
42
+ class BufferTooLargeError < ResourceLimitError
43
+ def initialize(**context)
44
+ super("FLEXR-E022", "streaming buffer exceeds max_buffer_size",
45
+ help: "increase max_buffer_size, shorten tokens, or use retain_input: false", **context)
46
+ end
47
+ end
48
+
49
+ class StepLimitError < ResourceLimitError
50
+ def initialize(**context)
51
+ super("FLEXR-E023", "lexer exceeds max_steps",
52
+ help: "increase max_steps or simplify the specification", **context)
53
+ end
54
+ end
55
+
56
+ class LookaheadTooLargeError < ResourceLimitError
57
+ def initialize(**context)
58
+ super("FLEXR-E024", "trailing context exceeds max_lookahead_size",
59
+ help: "increase max_lookahead_size or bound the trailing context", **context)
60
+ end
61
+ end
62
+
63
+ class StateStackOverflowError < ResourceLimitError
64
+ def initialize(message = "state stack exceeds max_state_stack", **context)
65
+ super("FLEXR-E025", message, help: "increase max_state_stack or remove recursive pushes", **context)
66
+ end
67
+ end
68
+
69
+ class CancelledError < ResourceLimitError
70
+ def initialize(**context)
71
+ super("FLEXR-E026", "lexing was cancelled", help: "resume with a new lexer when ready", **context)
72
+ end
73
+ end
74
+
75
+ class InvalidRecoveryActionError < ResourceLimitError
76
+ def initialize(action:, **context)
77
+ super("FLEXR-E027", "on_error returned unsupported action #{action.inspect}",
78
+ help: "return :skip, :raise, :halt, or :token", **context)
79
+ end
80
+ end
21
81
  end
22
82
  end