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
@@ -12,6 +12,7 @@ module Flexr
12
12
  "word" => "Word", "space" => "Space"
13
13
  }.freeze
14
14
  POSIX_CLASSES = %w[alnum alpha blank cntrl digit graph lower print punct space upper xdigit].freeze
15
+ LITERAL_ESCAPES = (%w[. [ ] { } ( ) * + ? | ^ $ \\ / - #] + [" "]).freeze
15
16
 
16
17
  attr_reader :source
17
18
 
@@ -22,6 +23,12 @@ module Flexr
22
23
  @unicode = unicode
23
24
  @index = 0
24
25
  @class_depth = 0
26
+ offset = 0
27
+ @byte_offsets = [0]
28
+ source.each_char do |character|
29
+ offset += character.bytesize
30
+ @byte_offsets << offset
31
+ end
25
32
  end
26
33
 
27
34
  def parse
@@ -34,43 +41,46 @@ module Flexr
34
41
  private
35
42
 
36
43
  def parse_expression
44
+ starting = @index
37
45
  branches = [parse_sequence]
38
46
  branches << parse_sequence while consume?("|")
39
47
  return branches.first if branches.length == 1
40
48
 
41
- AST::Alt.new(children: branches, loc: nil)
49
+ AST::Alt.new(children: branches, loc: span(starting))
42
50
  end
43
51
 
44
52
  def parse_sequence
53
+ starting = @index
45
54
  children = []
46
55
  children << parse_quantified until eof? || [")", "|"].include?(current)
47
- return AST::Empty.new(loc: nil) if children.empty?
56
+ return AST::Empty.new(loc: span(starting)) if children.empty?
48
57
  return children.first if children.length == 1
49
58
 
50
- AST::Seq.new(children: children, loc: nil)
59
+ AST::Seq.new(children: children, loc: span(starting))
51
60
  end
52
61
 
53
62
  def parse_quantified
63
+ starting = @index
54
64
  atom = parse_atom
55
65
  return atom unless ["*", "+", "?", "{"].include?(current)
56
66
 
57
67
  if consume?("*")
58
68
  reject_postfix_quantifier
59
- return AST::Star.new(child: atom, loc: nil)
69
+ return AST::Repeat.new(child: atom, minimum: 0, maximum: nil, loc: span(starting))
60
70
  end
61
71
  if consume?("+")
62
72
  reject_postfix_quantifier
63
- return AST::Seq.new(children: [atom, AST::Star.new(child: atom, loc: nil)], loc: nil)
73
+ return AST::Repeat.new(child: atom, minimum: 1, maximum: nil, loc: span(starting))
64
74
  end
65
75
  if consume?("?")
66
76
  reject_postfix_quantifier
67
- return AST::Alt.new(children: [atom, AST::Empty.new(loc: nil)], loc: nil)
77
+ return AST::Repeat.new(child: atom, minimum: 0, maximum: 1, loc: span(starting))
68
78
  end
69
79
 
70
- parse_repetition(atom)
80
+ parse_repetition(atom, starting)
71
81
  end
72
82
 
73
- def parse_repetition(atom)
83
+ def parse_repetition(atom, starting)
74
84
  consume?("{")
75
85
  min = read_number
76
86
  max = if consume?(",")
@@ -80,8 +90,7 @@ module Flexr
80
90
  end
81
91
  expect("}")
82
92
  raise_syntax("invalid repetition") if min.nil? || (!max.nil? && max < min)
83
- raise_syntax("open repetition is not supported") if max.nil?
84
- if max > 1000
93
+ if (max || min) > 1000
85
94
  raise_diagnostic(
86
95
  diagnostic("FLEXR-E007", "repetition limit exceeds 1000",
87
96
  help: "split the rule or use a smaller bounded repetition")
@@ -89,18 +98,11 @@ module Flexr
89
98
  end
90
99
  reject_postfix_quantifier
91
100
 
92
- required = Array.new(min) { atom }
93
- optional = Array.new(max - min) do
94
- AST::Alt.new(children: [atom, AST::Empty.new(loc: nil)], loc: nil)
95
- end
96
- children = required + optional
97
- return AST::Empty.new(loc: nil) if children.empty?
98
- return children.first if children.length == 1
99
-
100
- AST::Seq.new(children: children, loc: nil)
101
+ AST::Repeat.new(child: atom, minimum: min, maximum: max, loc: span(starting))
101
102
  end
102
103
 
103
104
  def parse_atom
105
+ starting = @index
104
106
  @escaped_value = false
105
107
  @last_ranges = nil
106
108
  return parse_group if consume?("(")
@@ -109,25 +111,28 @@ module Flexr
109
111
 
110
112
  if consume?(".")
111
113
  upper = @options.nobits?(::Regexp::MULTILINE) ? 0x0a - 1 : 0x10ffff
112
- return AST::CharClass.new(ranges: [[0, upper], [0x0b, 0x10ffff]], negated: false, loc: nil)
114
+ return AST::CharClass.new(
115
+ ranges: [[0, upper], [0x0b, 0x10ffff]], negated: false, loc: span(starting)
116
+ )
113
117
  end
114
118
 
115
119
  if consume?("\\")
116
120
  parse_escape
117
- return AST::CharClass.new(ranges: @last_ranges, negated: false, loc: nil) if @last_ranges
118
- return codepoint_node(read_codepoint) if @escaped_value
121
+ return AST::CharClass.new(ranges: @last_ranges, negated: false, loc: span(starting)) if @last_ranges
122
+ return codepoint_node(read_codepoint, span(starting)) if @escaped_value
119
123
  end
120
124
 
121
125
  char = advance
122
126
  raise_syntax("unexpected end of expression") unless char
123
- codepoint_node(char.ord)
127
+ codepoint_node(char.ord, span(starting))
124
128
  end
125
129
 
126
130
  def parse_group
131
+ starting = @index - 1
127
132
  saved_options = @options
128
133
  if consume?("?")
129
134
  prefix = parse_group_prefix
130
- return AST::Empty.new(loc: nil) if prefix == :global
135
+ return AST::Empty.new(loc: span(starting)) if prefix == :global
131
136
  else
132
137
  warn_capture
133
138
  end
@@ -174,10 +179,11 @@ module Flexr
174
179
  @options = enabled ? (@options | bit) : (@options & ~bit)
175
180
  end
176
181
 
177
- def codepoint_node(codepoint)
178
- return AST::CodepointRange.new(lo: codepoint, hi: codepoint, loc: nil) if @options.nobits?(::Regexp::IGNORECASE)
182
+ def codepoint_node(codepoint, location)
183
+ return AST::CodepointRange.new(lo: codepoint, hi: codepoint, loc: location) if
184
+ @options.nobits?(::Regexp::IGNORECASE)
179
185
 
180
- AST::CharClass.new(ranges: fold_ranges([[codepoint, codepoint]]), negated: false, loc: nil)
186
+ AST::CharClass.new(ranges: fold_ranges([[codepoint, codepoint]]), negated: false, loc: location)
181
187
  end
182
188
 
183
189
  def fold_ranges(ranges)
@@ -190,29 +196,51 @@ module Flexr
190
196
  end
191
197
 
192
198
  def parse_class
199
+ starting = @index - 1
200
+ @class_depth += 1
193
201
  negated = consume?("^")
202
+ ranges = parse_class_union
203
+ while peek_prefix?("&&")
204
+ @index += 2
205
+ ranges = materialize_class_ranges(ranges, negated: negated)
206
+ negated = false
207
+ right = if consume?("[")
208
+ nested = parse_class
209
+ materialize_class_ranges(nested.ranges, negated: nested.negated)
210
+ else
211
+ materialize_class_ranges(parse_class_union, negated: false)
212
+ end
213
+ ranges = intersect_ranges(ranges, right)
214
+ end
215
+ expect("]")
216
+ ranges = merge_ranges(ranges)
217
+ ranges = fold_ranges(ranges) if @options.anybits?(::Regexp::IGNORECASE)
218
+ AST::CharClass.new(ranges: ranges, negated: negated, loc: span(starting))
219
+ ensure
220
+ @class_depth -= 1
221
+ end
222
+
223
+ def parse_class_union
194
224
  ranges = []
195
- @class_depth += 1
196
- until eof? || current == "]"
225
+ until eof? || current == "]" || peek_prefix?("&&")
197
226
  if current == "[" && @source[@index, 2] == "[:"
198
227
  ranges.concat(parse_posix_class)
199
228
  next
200
229
  end
201
230
  first = parse_class_atom
202
- if consume?("-") && current != "]"
203
- last = parse_class_atom
204
- ranges.concat(expand_class_range(first, last))
231
+ if consume?("-")
232
+ if current == "]"
233
+ ranges.concat(first)
234
+ ranges << [45, 45]
235
+ else
236
+ last = parse_class_atom
237
+ ranges.concat(expand_class_range(first, last))
238
+ end
205
239
  else
206
240
  ranges.concat(first)
207
241
  end
208
242
  end
209
- expect("]")
210
- @class_depth -= 1
211
- ranges = merge_ranges(ranges)
212
- ranges = fold_ranges(ranges) if @options.anybits?(::Regexp::IGNORECASE)
213
- AST::CharClass.new(ranges: ranges, negated: negated, loc: nil)
214
- ensure
215
- @class_depth -= 1 if @class_depth.positive? && current != "]"
243
+ ranges
216
244
  end
217
245
 
218
246
  def parse_posix_class
@@ -261,12 +289,46 @@ module Flexr
261
289
  [[lo, hi]]
262
290
  end
263
291
 
292
+ def materialize_class_ranges(ranges, negated:)
293
+ concrete = ranges.flat_map do |range|
294
+ if range.first == AST::Property
295
+ Unicode::Property.ranges(range[2], negate: range[1])
296
+ else
297
+ [range]
298
+ end
299
+ end
300
+ concrete = merge_ranges(concrete)
301
+ negated ? complement_ranges(concrete) : concrete
302
+ end
303
+
304
+ def intersect_ranges(left, right)
305
+ result = []
306
+ left_index = 0
307
+ right_index = 0
308
+ while left_index < left.length && right_index < right.length
309
+ left_range = left[left_index]
310
+ right_range = right[right_index]
311
+ lo = [left_range.first, right_range.first].max
312
+ hi = [left_range.last, right_range.last].min
313
+ result << [lo, hi] if lo <= hi
314
+ if left_range.last < right_range.last
315
+ left_index += 1
316
+ else
317
+ right_index += 1
318
+ end
319
+ end
320
+ result
321
+ end
322
+
264
323
  def parse_escape
265
324
  @escaped_value = false
266
325
  @last_ranges = nil
267
326
  char = advance_raw
268
327
  raise_syntax("trailing backslash") unless char
269
328
  if ESCAPES.key?(char)
329
+ raise unsupported("octal escapes", "use an explicit hexadecimal escape") if
330
+ char == "0" && current&.match?(/[0-9]/)
331
+
270
332
  @escaped_value = true
271
333
  @last_codepoint = ESCAPES.fetch(char)
272
334
  return
@@ -299,14 +361,22 @@ module Flexr
299
361
  end
300
362
  assign_codepoint(digits)
301
363
  nil
302
- when "G", "K", "b", "B", "A", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9"
364
+ when "b"
365
+ raise unsupported("\\b", "use an explicit token boundary rule") unless @class_depth.positive?
366
+
367
+ @escaped_value = true
368
+ @last_codepoint = 0x08
369
+ when "G", "K", "B", "A", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9",
370
+ "g", "R", "X", "c", "C", "M"
303
371
  raise unsupported("\\#{char}", "use a state or followed_by: instead")
304
372
  when "k"
305
373
  read_until(">") if consume?("<")
306
374
  raise unsupported("backreferences", "split the rule into DFA-compatible states")
307
- else
375
+ when *LITERAL_ESCAPES
308
376
  @escaped_value = true
309
377
  @last_codepoint = char.ord
378
+ else
379
+ raise unsupported("unknown escape \\#{char}", "remove the backslash or use a supported escape")
310
380
  end
311
381
  end
312
382
 
@@ -320,7 +390,7 @@ module Flexr
320
390
  "d" => [[48, 57]],
321
391
  "w" => [[48, 57], [65, 90], [95, 95], [97, 122]],
322
392
  "s" => [[9, 13], [32, 32]],
323
- "h" => [[9, 9], [32, 32]]
393
+ "h" => [[48, 57], [65, 70], [97, 102]]
324
394
  }.fetch(char.downcase)
325
395
  return base unless char == char.upcase
326
396
 
@@ -339,10 +409,11 @@ module Flexr
339
409
  end
340
410
 
341
411
  def parse_anchor
412
+ starting = @index
342
413
  char = advance
343
- return AST::Anchor.new(kind: :bol, loc: nil) if char == "^"
414
+ return AST::Anchor.new(kind: :bol, loc: span(starting)) if char == "^"
344
415
 
345
- AST::Anchor.new(kind: :eol, loc: nil)
416
+ AST::Anchor.new(kind: :eol, loc: span(starting))
346
417
  end
347
418
 
348
419
  def validate_anchor_positions(node)
@@ -375,7 +446,7 @@ module Flexr
375
446
 
376
447
  def anchor_nodes(node)
377
448
  return [node] if node.is_a?(AST::Anchor)
378
- return anchor_nodes(node.child) if node.is_a?(AST::Star)
449
+ return anchor_nodes(node.child) if node.is_a?(AST::Star) || node.is_a?(AST::Repeat)
379
450
  return [] unless node.respond_to?(:children)
380
451
 
381
452
  node.children.flat_map { |child| anchor_nodes(child) }
@@ -383,7 +454,7 @@ module Flexr
383
454
 
384
455
  def anchor_nested_in_alternative?(node)
385
456
  return node.children.any? { |child| anchor_nodes(child).any? } if node.is_a?(AST::Alt)
386
- return anchor_nested_in_alternative?(node.child) if node.is_a?(AST::Star)
457
+ return anchor_nested_in_alternative?(node.child) if node.is_a?(AST::Star) || node.is_a?(AST::Repeat)
387
458
  return false unless node.respond_to?(:children)
388
459
 
389
460
  node.children.any? { |child| anchor_nested_in_alternative?(child) }
@@ -512,6 +583,10 @@ module Flexr
512
583
  @index += 1 while @source[@index] && @source[@index] != "\n"
513
584
  end
514
585
  end
586
+
587
+ def span(starting, ending = @index)
588
+ @byte_offsets.fetch(starting)...@byte_offsets.fetch(ending)
589
+ end
515
590
  end
516
591
  end
517
592
  end