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
@@ -13,25 +13,34 @@ module Flexr
13
13
  class Interpreter
14
14
  def initialize(lexer)
15
15
  @lexer = lexer
16
+ klass = lexer.class
17
+ @compiled = klass.__flexr_compiled
18
+ @rules = klass.__flexr_rules
19
+ @config = klass.__flexr_config
16
20
  end
17
21
 
18
22
  def scan
19
- machine = @lexer.class.__flexr_compiled.machines.fetch(@lexer.state)
23
+ machine = @compiled.machines.fetch(@lexer.state)
20
24
  position = @lexer.byte_pos
21
25
  return nil unless @lexer.valid_utf8_at?(position)
22
- return scan_firstmatch(position) if @lexer.class.__flexr_config.backend == :firstmatch
23
- return scan_fast(machine, position) if fast_path?
26
+ return scan_firstmatch(position) if @config.backend == :firstmatch
27
+ return scan_fast(machine, position) if fast_path?(machine)
24
28
 
25
29
  buffer = @lexer.buffer
26
30
  state = machine.dfa.start
27
31
  best = reference_match(position, buffer)
28
32
  cursor = position
29
- best = consider_acceptances(machine, state, cursor, position, buffer, best) if @lexer.class.__flexr_config.options[:allow_empty_match]
33
+ best = consider_acceptances(machine, state, cursor, position, buffer, best) if @config.options[:allow_empty_match]
30
34
  acceleration_regions = acceleration_enabled? ? acceleration_regions_for(machine) : nil
31
35
 
32
36
  while buffer.ensure_available?(cursor + 1)
33
- if acceleration_regions && (region = acceleration_regions[state])
34
- accelerated_end = accelerate(region, buffer, cursor)
37
+ if acceleration_regions && (region = acceleration_regions[state]) &&
38
+ !@disabled_auto_acceleration&.key?(region)
39
+ acceptance = machine.dfa.accepts[state].first
40
+ token_rule = acceptance&.rule_index
41
+ accelerated_end = accelerate(
42
+ region, buffer, cursor, token_rule: token_rule, token_start: position
43
+ )
35
44
  if accelerated_end && accelerated_end > cursor
36
45
  cursor = accelerated_end
37
46
  best = consider_acceptances(machine, state, cursor, position, buffer, best)
@@ -40,10 +49,13 @@ module Flexr
40
49
  end
41
50
 
42
51
  byte = buffer.getbyte(cursor)
52
+ @lexer.consume_step!
43
53
  state = transition(machine.dfa, state, byte)
44
54
  break unless state
45
55
 
46
56
  cursor += 1
57
+ acceptance = machine.dfa.accepts[state].first
58
+ ensure_token_size!(cursor, position, rule: acceptance&.rule_index)
47
59
  best = consider_acceptances(machine, state, cursor, position, buffer, best)
48
60
  end
49
61
  best
@@ -51,22 +63,57 @@ module Flexr
51
63
 
52
64
  def scan_fast(machine, position)
53
65
  buffer = @lexer.buffer
54
- source = buffer.source
55
66
  dfa = machine.dfa
56
67
  accepts = dfa.accepts
57
- rules = @lexer.class.__flexr_rules
68
+ rules = @rules
58
69
  transitions = dfa.transitions
59
70
  ec = dfa.ec
60
71
  direct = dfa.direct
72
+ direct_nxt = direct&.fetch(:nxt)
73
+ direct_classes = direct&.fetch(:classes)
74
+ source = buffer.stable_source
75
+ guarded_steps = @lexer.scan_steps_guarded?
76
+ text_start = @lexer.more_text_start || position
77
+ max_token_size = @lexer.max_token_size
78
+ token_limit_required = @lexer.token_limit_required?
61
79
  state = dfa.start
62
80
  cursor = position
63
81
  best = nil
64
82
 
65
- while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)
66
- byte = source.getbyte(cursor)
67
- state = if direct
83
+ if source && !guarded_steps && !token_limit_required
84
+ while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)
85
+ byte = source.getbyte(cursor)
86
+ state = if direct_nxt
87
+ class_id = ec[byte]
88
+ value = direct_nxt[(state * direct_classes) + class_id]
89
+ value >= 0 ? value : nil
90
+ else
91
+ transitions[state][ec[byte]]
92
+ end
93
+ break unless state
94
+
95
+ cursor += 1
96
+ acceptance = accepts[state].first
97
+ next unless acceptance
98
+
99
+ rule = rules.fetch(acceptance.rule_index)
100
+ next if best && cursor == best.total_end_pos && rule.index > best.rule.index
101
+
102
+ best ||= (@match ||= Match.new)
103
+ best.rule = rule
104
+ best.start_pos = position
105
+ best.end_pos = cursor
106
+ best.total_end_pos = cursor
107
+ end
108
+ return best
109
+ end
110
+
111
+ while cursor < buffer.bytesize || buffer.ensure_available?(cursor + 1)
112
+ byte = buffer.getbyte(cursor)
113
+ @lexer.consume_step! if guarded_steps
114
+ state = if direct_nxt
68
115
  class_id = ec[byte]
69
- value = direct[:nxt][(state * direct[:classes]) + class_id]
116
+ value = direct_nxt[(state * direct_classes) + class_id]
70
117
  value >= 0 ? value : nil
71
118
  else
72
119
  transitions[state][ec[byte]]
@@ -75,6 +122,10 @@ module Flexr
75
122
 
76
123
  cursor += 1
77
124
  acceptance = accepts[state].first
125
+ if token_limit_required
126
+ size = cursor - text_start
127
+ @lexer.defer_token_size_check!(size, rule: acceptance&.rule_index) if size > max_token_size
128
+ end
78
129
  next unless acceptance
79
130
 
80
131
  rule = rules.fetch(acceptance.rule_index)
@@ -94,7 +145,7 @@ module Flexr
94
145
  def reference_match(position, buffer)
95
146
  return unless reference_rules?
96
147
 
97
- candidates = @lexer.class.__flexr_rules.filter_map do |rule|
148
+ candidates = @rules.filter_map do |rule|
98
149
  next unless rule_active?(rule)
99
150
  next unless rule.patterns.any? { |pattern| reference_pattern?(pattern) }
100
151
 
@@ -102,7 +153,10 @@ module Flexr
102
153
  condition = rule.pattern_conditions.fetch(pattern_index)
103
154
  next if condition.bol_only && !@lexer.beginning_of_line?
104
155
 
105
- match = streamed_match(pattern, buffer, position, reference: reference_pattern?(pattern))
156
+ match = streamed_match(
157
+ pattern, buffer, position, reference: reference_pattern?(pattern),
158
+ limit: :token, rule: rule
159
+ )
106
160
  next unless match&.begin(0)&.zero?
107
161
 
108
162
  end_position = position + match[0].bytesize
@@ -120,7 +174,7 @@ module Flexr
120
174
  trailing = trailing_length(rule, buffer, end_position)
121
175
  next if rule.trailing && trailing.nil?
122
176
 
123
- ensure_token_size!(end_position, position)
177
+ ensure_token_size!(end_position, position, rule: rule)
124
178
  Match.new(rule: rule, start_pos: position, end_pos: end_position,
125
179
  total_end_pos: end_position + (trailing || 0))
126
180
  end
@@ -128,18 +182,20 @@ module Flexr
128
182
  end
129
183
 
130
184
  def reference_rules?
185
+ return false unless @config.backend == :firstmatch
131
186
  return @reference_rules unless @reference_rules.nil?
132
187
 
133
- @reference_rules = @lexer.class.__flexr_rules.any? do |rule|
188
+ @reference_rules = @rules.any? do |rule|
134
189
  rule.patterns.any? { |pattern| reference_pattern?(pattern) }
135
190
  end
136
191
  end
137
192
 
138
- def fast_path?
139
- return @fast_path unless @fast_path.nil?
193
+ def fast_path?(machine)
194
+ @fast_paths ||= {}
195
+ return @fast_paths[machine.dfa] if @fast_paths.key?(machine.dfa)
140
196
 
141
- rules = @lexer.class.__flexr_rules
142
- @fast_path = !@lexer.class.__flexr_config.options[:allow_empty_match] &&
197
+ rules = machine.dfa.rule_ids.map { |rule_index| @rules.fetch(rule_index) }
198
+ @fast_paths[machine.dfa] = !@config.options[:allow_empty_match] &&
143
199
  !reference_rules? && rules.none?(&:trailing) &&
144
200
  rules.none? { |rule| rule.pattern_conditions.any? { |condition| condition&.bol_only || condition&.end_anchor } }
145
201
  end
@@ -148,13 +204,13 @@ module Flexr
148
204
  return false unless pattern.is_a?(::Regexp)
149
205
  return true if pattern.source.match?(/\\[pP]\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
150
206
 
151
- @lexer.class.__flexr_config.options[:unicode] == true && @lexer.utf8_input? &&
207
+ @config.options[:unicode] == true && @lexer.utf8_input? &&
152
208
  pattern.source.match?(/\\[dDwWsS]/)
153
209
  end
154
210
 
155
211
  def scan_firstmatch(position)
156
212
  buffer = @lexer.buffer
157
- @lexer.class.__flexr_rules.sort_by(&:index).each do |rule|
213
+ @rules.sort_by(&:index).each do |rule|
158
214
  next unless rule_active?(rule)
159
215
 
160
216
  matches = rule.patterns.each_with_index.filter_map do |pattern, pattern_index|
@@ -162,10 +218,11 @@ module Flexr
162
218
  next if condition.bol_only && !@lexer.beginning_of_line?
163
219
 
164
220
  regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
165
- streamed_match(regexp, buffer, position, reference: reference_pattern?(regexp))
221
+ streamed_match(
222
+ regexp, buffer, position, reference: reference_pattern?(regexp),
223
+ limit: :token, rule: rule
224
+ )
166
225
  &.then { |match| [match, condition] }
167
- rescue ArgumentError, RegexpError
168
- nil
169
226
  end
170
227
  match, condition = matches.select { |candidate| candidate[0].begin(0).zero? }
171
228
  .max_by { |candidate| candidate[0][0].bytesize }
@@ -177,7 +234,7 @@ module Flexr
177
234
  trailing = trailing_length(rule, buffer, end_position)
178
235
  next if rule.trailing && trailing.nil?
179
236
 
180
- ensure_token_size!(end_position, position)
237
+ ensure_token_size!(end_position, position, rule: rule)
181
238
  return reusable_match(rule, position, end_position, end_position + (trailing || 0))
182
239
  end
183
240
  nil
@@ -187,27 +244,22 @@ module Flexr
187
244
  return 0 unless rule.trailing
188
245
 
189
246
  regexp = rule.trailing
190
- match = streamed_match(regexp, buffer, position, reference: reference_pattern?(regexp))
247
+ match = streamed_match(
248
+ regexp, buffer, position, reference: reference_pattern?(regexp),
249
+ limit: :lookahead, rule: rule
250
+ )
191
251
  return nil unless match&.begin(0)&.zero?
192
252
 
193
- match[0].bytesize
194
- rescue ArgumentError
195
- nil
253
+ match[0].bytesize.tap { |size| @lexer.ensure_lookahead_size!(size, rule: rule) }
196
254
  end
197
255
 
198
- def streamed_match(pattern, buffer, position, reference: false)
256
+ def streamed_match(pattern, buffer, position, limit:, rule:, reference: false)
199
257
  pattern = regexp_pattern(pattern)
200
258
  minimum = minimum_match_bytes(pattern)
201
259
  loop do
202
260
  subject, tail = stream_subject(buffer, position)
203
- match = if reference
204
- Unicode::ReferenceRegexp.match(
205
- pattern, subject, encoding: @lexer.class.__flexr_config.encoding,
206
- options: pattern.options, unicode: @lexer.class.__flexr_config.options[:unicode] == true
207
- )
208
- else
209
- pattern.match(subject, 0)
210
- end
261
+ match = match_stream_pattern(pattern, subject, reference: reference)
262
+ ensure_stream_limit!(limit, position, match[0].bytesize, rule) if match
211
263
  return match if match && match[0].bytesize < subject.bytesize
212
264
  return match if match && %i[eof invalid].include?(tail)
213
265
  if !match && subject.bytesize >= minimum && tail != :incomplete
@@ -215,12 +267,25 @@ module Flexr
215
267
  return nil unless first_byte && possible_first_byte?(pattern, first_byte)
216
268
  return nil if %i[eof invalid].include?(tail)
217
269
  end
270
+ ensure_stream_limit!(limit, position, subject.bytesize, rule) unless match
271
+ @lexer.consume_step!
218
272
  return match unless can_refill_match?(buffer, position, subject.bytesize, tail)
219
- rescue ArgumentError, RegexpError
220
- return nil
221
273
  end
222
274
  end
223
275
 
276
+ def match_stream_pattern(pattern, subject, reference:)
277
+ if reference
278
+ Unicode::ReferenceRegexp.match(
279
+ pattern, subject, encoding: @config.encoding,
280
+ options: pattern.options, unicode: @config.options[:unicode] == true
281
+ )
282
+ else
283
+ pattern.match(subject, 0)
284
+ end
285
+ rescue ArgumentError, RegexpError
286
+ nil
287
+ end
288
+
224
289
  def stream_subject(buffer, position)
225
290
  return [buffer.byteslice(position...buffer.bytesize).to_s.b, buffer.eof_loaded? ? :eof : :end] unless @lexer.utf8_input?
226
291
 
@@ -245,7 +310,7 @@ module Flexr
245
310
  def utf8_status(buffer, position)
246
311
  return [:eof, 0] if position >= buffer.bytesize
247
312
 
248
- first = buffer.source.getbyte(position)
313
+ first = buffer.getbyte(position)
249
314
  return [:complete, 1] if first <= 0x7f
250
315
  return [:invalid, 1] unless first.between?(0xc2, 0xf4)
251
316
 
@@ -289,7 +354,7 @@ module Flexr
289
354
 
290
355
  def minimum_ast_bytes(node, ignorecase: false)
291
356
  case node
292
- when Regexp::AST::Empty, Regexp::AST::Anchor then 0
357
+ when Regexp::AST::Empty, Regexp::AST::Anchor, Regexp::AST::Fail then 0
293
358
  when Regexp::AST::ByteRange then 1
294
359
  when Regexp::AST::CodepointRange then utf8_length(node.lo)
295
360
  when Regexp::AST::CharClass
@@ -300,6 +365,7 @@ module Flexr
300
365
  ranges.map { |lo, _hi| utf8_length(lo) }.min || 1
301
366
  when Regexp::AST::Seq then node.children.sum { |child| minimum_ast_bytes(child, ignorecase: ignorecase) }
302
367
  when Regexp::AST::Alt then node.children.map { |child| minimum_ast_bytes(child, ignorecase: ignorecase) }.min || 0
368
+ when Regexp::AST::Repeat then node.minimum * minimum_ast_bytes(node.child, ignorecase: ignorecase)
303
369
  else minimum_unknown_bytes(node)
304
370
  end
305
371
  end
@@ -323,7 +389,7 @@ module Flexr
323
389
 
324
390
  def first_byte_ranges(node, options, binary: false)
325
391
  case node
326
- when Regexp::AST::Empty, Regexp::AST::Anchor then []
392
+ when Regexp::AST::Empty, Regexp::AST::Anchor, Regexp::AST::Fail then []
327
393
  when Regexp::AST::ByteRange then [[node.lo, node.hi]]
328
394
  when Regexp::AST::CodepointRange
329
395
  ranges = options.anybits?(::Regexp::IGNORECASE) ? Unicode::CaseFold.ranges(node.lo, node.hi) : [[node.lo, node.hi]]
@@ -349,7 +415,7 @@ module Flexr
349
415
  ranges
350
416
  when Regexp::AST::Alt
351
417
  node.children.flat_map { |child| first_byte_ranges(child, options, binary: binary) }
352
- when Regexp::AST::Star
418
+ when Regexp::AST::Star, Regexp::AST::Repeat
353
419
  first_byte_ranges(node.child, options, binary: binary)
354
420
  else
355
421
  first_byte_fallback
@@ -363,6 +429,7 @@ module Flexr
363
429
  def nullable?(node)
364
430
  case node
365
431
  when Regexp::AST::Empty, Regexp::AST::Anchor, Regexp::AST::Star then true
432
+ when Regexp::AST::Repeat then node.minimum.zero? || nullable?(node.child)
366
433
  when Regexp::AST::Seq then node.children.all? { |child| nullable?(child) }
367
434
  when Regexp::AST::Alt then node.children.any? { |child| nullable?(child) }
368
435
  else false
@@ -400,7 +467,7 @@ module Flexr
400
467
 
401
468
  def consider_acceptances(machine, state, cursor, position, buffer, best)
402
469
  machine.dfa.accepts[state].each do |acceptance|
403
- candidate = @lexer.class.__flexr_rules.fetch(acceptance.rule_index)
470
+ candidate = @rules.fetch(acceptance.rule_index)
404
471
  next if acceptance.bol_only && !@lexer.beginning_of_line?
405
472
  next unless @lexer.utf8_boundary?(cursor)
406
473
  next unless !acceptance.end_anchor || end_anchor_match?(buffer, cursor)
@@ -412,7 +479,7 @@ module Flexr
412
479
  next if best && total_end < best.total_end_pos
413
480
  next if best && total_end == best.total_end_pos && acceptance.rule_index > best.rule.index
414
481
 
415
- ensure_token_size!(cursor, position)
482
+ ensure_token_size!(cursor, position, rule: candidate)
416
483
  best = @match ||= Match.new
417
484
  best.rule = candidate
418
485
  best.start_pos = position
@@ -423,7 +490,8 @@ module Flexr
423
490
  end
424
491
 
425
492
  def acceleration_enabled?
426
- @lexer.class.__flexr_config.options.fetch(:accel, :auto) != :none
493
+ mode = @config.options.fetch(:accel, :auto)
494
+ mode != :none && !(mode == :auto && @auto_acceleration_off)
427
495
  end
428
496
 
429
497
  def reusable_match(rule, start_pos, end_pos, total_end_pos)
@@ -435,32 +503,63 @@ module Flexr
435
503
  @match
436
504
  end
437
505
 
438
- def accelerate(region, buffer, position)
439
- mode = @lexer.class.__flexr_config.options.fetch(:accel, :auto)
440
- binary = buffer.source.b
441
- match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)
442
- scanner = StringScanner.new(binary)
443
- scanner.pos = position
444
- length = scanner.skip(region.regexp)
445
- length && scanner.pos
446
- else
447
- region.regexp.match(binary, position)&.then { |match| match.begin(0) == position ? match.end(0) : nil }
506
+ def accelerate(region, buffer, position, token_rule:, token_start:)
507
+ mode = @config.options.fetch(:accel, :auto)
508
+ cursor = position
509
+ matched = false
510
+ loop do
511
+ if cursor >= buffer.bytesize
512
+ return matched ? cursor : nil if buffer.eof_loaded?
513
+ return matched ? cursor : nil unless buffer.ensure_available?(cursor + 1)
514
+ end
515
+
516
+ segment = buffer.source
517
+ segment_position = cursor - buffer.base_offset
518
+ length = acceleration_length(region, segment, segment_position, mode)
519
+ record_auto_acceleration_miss(region) if mode == :auto && length.to_i < 8
520
+ return matched ? cursor : nil unless length&.positive?
521
+
522
+ matched = true
523
+ cursor += length
524
+ @lexer.consume_step!(length)
525
+ ensure_token_size!(cursor, token_start, rule: token_rule)
526
+ return cursor if cursor < buffer.bytesize || buffer.eof_loaded?
448
527
  end
449
- return match_end if match_end && match_end < buffer.bytesize
450
- return match_end if match_end && buffer.eof_loaded?
451
- return unless buffer.ensure_available?(buffer.bytesize + 1)
528
+ end
452
529
 
453
- accelerate(region, buffer, position)
530
+ def acceleration_length(region, segment, position, mode)
531
+ if segment.encoding == Encoding::UTF_8 && region.utf8_regexp
532
+ regexp = region.utf8_regexp
533
+ else
534
+ segment = segment.b unless segment.encoding == Encoding::BINARY
535
+ regexp = region.regexp
536
+ end
537
+ if %i[strscan auto].include?(mode) && defined?(::StringScanner)
538
+ @acceleration_scanner ||= ::StringScanner.new("".b)
539
+ @acceleration_scanner.string = segment
540
+ @acceleration_scanner.pos = position
541
+ @acceleration_scanner.skip(regexp)
542
+ else
543
+ match = regexp.match(segment, position)
544
+ match&.begin(0) == position ? match.end(0) - position : nil
545
+ end
454
546
  rescue ArgumentError
455
547
  nil
456
548
  end
457
549
 
550
+ def record_auto_acceleration_miss(region)
551
+ @auto_acceleration_misses ||= Hash.new(0)
552
+ @auto_acceleration_misses[region] += 1
553
+ @auto_acceleration_miss_total = @auto_acceleration_miss_total.to_i + 1
554
+ @auto_acceleration_off = true if @auto_acceleration_miss_total >= 9
555
+ return if @auto_acceleration_misses[region] < 3
556
+
557
+ @disabled_auto_acceleration ||= {}
558
+ @disabled_auto_acceleration[region] = true
559
+ end
560
+
458
561
  def transition(dfa, state, byte)
459
- if @lexer.class.__flexr_config.backend == :direct &&
460
- @lexer.class.respond_to?(:__flexr_generated_direct_transition)
461
- return @lexer.class.__flexr_generated_direct_transition(@lexer.state, state, byte)
462
- end
463
- return dfa.transition_direct(state, byte) if @lexer.class.__flexr_config.backend == :direct
562
+ return dfa.transition_direct(state, byte) if @config.backend == :direct
464
563
 
465
564
  dfa.transition(state, byte)
466
565
  end
@@ -474,7 +573,7 @@ module Flexr
474
573
 
475
574
  accepting = machine.dfa.accepts[region.state]
476
575
  next if accepting.any? do |acceptance|
477
- rule = @lexer.class.__flexr_rules.fetch(acceptance.rule_index)
576
+ rule = @rules.fetch(acceptance.rule_index)
478
577
  acceptance.bol_only || acceptance.end_anchor || rule.trailing
479
578
  end
480
579
 
@@ -486,15 +585,23 @@ module Flexr
486
585
  buffer.eof?(position) || buffer.getbyte(position) == 0x0a
487
586
  end
488
587
 
489
- def ensure_token_size!(end_position, position)
588
+ def ensure_token_size!(end_position, position, rule: nil)
490
589
  text_start = @lexer.more_text_start || position
491
- @lexer.defer_token_size_check!(end_position - text_start)
590
+ @lexer.defer_token_size_check!(end_position - text_start, rule: rule)
591
+ end
592
+
593
+ def ensure_stream_limit!(limit, position, size, rule)
594
+ if limit == :lookahead
595
+ @lexer.ensure_lookahead_size!(size, rule: rule)
596
+ else
597
+ ensure_token_size!(position + size, position, rule: rule)
598
+ end
492
599
  end
493
600
 
494
601
  def rule_active?(rule)
495
602
  return true if rule.states.include?(:initial) && @lexer.state == :initial
496
603
 
497
- state = @lexer.class.__flexr_config.states.fetch(@lexer.state)
604
+ state = @config.states.fetch(@lexer.state)
498
605
  return true if state.inclusive && rule.states.include?(:initial)
499
606
 
500
607
  rule.states.include?(@lexer.state)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "monitor"
5
+ require_relative "version"
6
+ require_relative "errors"
7
+ require_relative "diagnostics"
8
+ require_relative "action_resolver"
9
+ require_relative "configuration"
10
+ require_relative "ir"
11
+ require_relative "unicode/version"
12
+ require_relative "regexp/ast"
13
+ require_relative "regexp/parser"
14
+ require_relative "regexp/normalizer"
15
+ require_relative "regexp/unsupported"
16
+ require_relative "regexp/char_class"
17
+ require_relative "unicode/utf8_splitter"
18
+ require_relative "unicode/data"
19
+ require_relative "unicode/property"
20
+ require_relative "unicode/reference_regexp"
21
+ require_relative "unicode/case_fold"
22
+ require_relative "automaton/types"
23
+ require_relative "automaton/byte_class_set"
24
+ require_relative "automaton/nfa"
25
+ require_relative "automaton/dfa"
26
+ require_relative "automaton/compiler"
27
+ require_relative "automaton/analysis"
28
+ require_relative "automaton/minimizer"
29
+ require_relative "automaton/backend_cost_model"
30
+ require_relative "automaton/accel"
31
+ require_relative "runtime/location"
32
+ require_relative "runtime/token"
33
+ require_relative "runtime/buffer"
34
+ require_relative "runtime/errors"
35
+ require_relative "runtime/interpreter"
36
+ require_relative "runtime/core"
37
+ require_relative "dsl"
38
+ require_relative "lexer"
39
+ require_relative "generated"
40
+
41
+ module Flexr
42
+ class << self
43
+ def compile_pattern(pattern, options: {})
44
+ regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(pattern.to_s)
45
+ encoding = regexp.encoding == Encoding::BINARY ? Encoding::BINARY : Encoding::UTF_8
46
+ rule = IR::Rule.new(index: 0, patterns: [regexp], action: :skip, states: [:initial])
47
+ state = IR::State.new(name: :initial, inclusive: true, id: 0)
48
+ spec = IR::Spec.new(
49
+ class_name: "Pattern", backend: :table, token_kind: :array,
50
+ encoding: encoding, options: options, declared_tokens: [],
51
+ states: { initial: state }, rules: [rule], eof_rules: {}, verbatim: nil
52
+ )
53
+ Automaton::Compiler.new(spec).compile.machines.fetch(:initial).dfa
54
+ end
55
+
56
+ def reference_pattern?(regexp, unicode: false)
57
+ return true if regexp.source.match?(/\\[pP]\{/) || regexp.source.match?(/\[:(?:\^)?[a-z]+:\]/)
58
+
59
+ unicode && regexp.encoding != Encoding::BINARY && regexp.source.match?(/\\[dDwWsS]/)
60
+ end
61
+
62
+ def parse_pattern(pattern, options: {})
63
+ regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(pattern.to_s)
64
+ encoding = regexp.encoding == Encoding::BINARY ? Encoding::BINARY : Encoding::UTF_8
65
+ ast = Regexp::Parser.new(regexp.source, options: regexp.options, encoding: encoding,
66
+ unicode: options[:unicode] == true).parse
67
+ Regexp::Normalizer.new(ast, encoding: encoding, options: regexp.options).normalize
68
+ end
69
+ end
70
+ end
@@ -3,29 +3,82 @@
3
3
  module Flexr
4
4
  module Source
5
5
  module Passthrough
6
+ ENCODING_COMMENT = /\A[ \t]*#.*(?:coding|encoding)\s*[:=]\s*[-A-Za-z0-9_]+/i
7
+ FROZEN_COMMENT = /\A[ \t]*#\s*frozen_string_literal\s*:\s*(?:true|false)\b/i
8
+
6
9
  module_function
7
10
 
8
11
  def remove_spans(source, spans, insertion:, payload:)
12
+ rewrite(source, spans.map { |start_offset, end_offset| [start_offset, end_offset, ""] },
13
+ insertion: insertion, payload: payload)
14
+ end
15
+
16
+ def rewrite(source, edits, insertion:, payload:)
9
17
  result = source.dup
10
- top_level = spans.reject do |span|
11
- spans.any? do |outer|
12
- next false if outer.equal?(span)
18
+ normalized_edits = edits.map do |start_offset, end_offset, replacement|
19
+ start_offset = line_start_offset(source, start_offset) if replacement.empty? && start_offset != insertion
20
+ [start_offset, end_offset, replacement]
21
+ end
22
+ validate_non_overlapping!(normalized_edits)
23
+ adjusted_insertion = insertion + normalized_edits.sum do |start_offset, end_offset, replacement|
24
+ next 0 if start_offset >= insertion
13
25
 
14
- outer.first <= span.first && span.last <= outer.last &&
15
- (outer.first < span.first || span.last < outer.last)
16
- end
26
+ replaced_size = [end_offset, insertion].min - start_offset
27
+ replacement.bytesize - replaced_size
17
28
  end
18
- top_level.sort_by(&:first).reverse_each do |start_offset, end_offset|
19
- result.slice!(start_offset...end_offset)
29
+ normalized_edits.sort_by(&:first).reverse_each do |start_offset, end_offset, replacement|
30
+ result[start_offset...end_offset] = replacement
20
31
  end
21
- result.insert(insertion, payload)
32
+ result.insert(adjusted_insertion, payload)
22
33
  result
23
34
  end
24
35
 
36
+ def insert_after_preamble(source, payload)
37
+ offset = 0
38
+ line, next_offset = next_line(source, offset)
39
+ if line&.start_with?("#!")
40
+ offset = next_offset
41
+ line, next_offset = next_line(source, offset)
42
+ end
43
+
44
+ while line && magic_comment?(line)
45
+ offset = next_offset
46
+ line, next_offset = next_line(source, offset)
47
+ end
48
+
49
+ source.dup.insert(offset, payload)
50
+ end
51
+
25
52
  def indentation(source, offset)
26
53
  line_start = source.rindex("\n", offset - 1)
27
54
  source[(line_start ? line_start + 1 : 0)...offset].to_s[/\A[ \t]*/].to_s
28
55
  end
56
+
57
+ def line_start_offset(source, offset)
58
+ line_start = source.rindex("\n", offset - 1) unless offset.zero?
59
+ line_start = line_start ? line_start + 1 : 0
60
+ prefix = source.byteslice(line_start...offset)
61
+ prefix&.match?(/\A[ \t]*\z/) ? line_start : offset
62
+ end
63
+
64
+ def next_line(source, offset)
65
+ return [nil, offset] if offset >= source.bytesize
66
+
67
+ ending = source.index("\n", offset)
68
+ ending ? [source.byteslice(offset..ending), ending + 1] : [source.byteslice(offset..), source.bytesize]
69
+ end
70
+
71
+ def magic_comment?(line)
72
+ ENCODING_COMMENT.match?(line) || FROZEN_COMMENT.match?(line)
73
+ end
74
+
75
+ def validate_non_overlapping!(edits)
76
+ edits.sort_by(&:first).each_cons(2) do |left, right|
77
+ next if left[1] <= right[0]
78
+
79
+ raise ArgumentError, "overlapping source edits"
80
+ end
81
+ end
29
82
  end
30
83
  end
31
84
  end