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.
- checksums.yaml +4 -4
- data/Rakefile +74 -28
- data/benchmark/golden/calculator_lexer.sha256 +1 -1
- data/benchmark/golden/json_lexer.sha256 +1 -1
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -1
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -1
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -1
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -1
- data/benchmark/golden/with_racc_lexer.sha256 +1 -1
- data/docs/README.md +2 -1
- data/docs/explanation/backends.md +13 -10
- data/docs/explanation/matching-semantics.md +4 -2
- data/docs/explanation/security-model.md +10 -3
- data/docs/explanation/unicode-and-encoding.md +3 -1
- data/docs/how-to/deploy-a-standalone-lexer.md +5 -3
- data/docs/how-to/generate-a-lexer.md +3 -3
- data/docs/how-to/handle-errors.md +4 -1
- data/docs/how-to/run-a-lexer-at-runtime.md +5 -0
- data/docs/how-to/track-token-locations.md +2 -1
- data/docs/how-to/tune-performance.md +3 -3
- data/docs/perf-log.md +16 -0
- data/docs/reference/actions.md +5 -2
- data/docs/reference/diagnostics.md +8 -0
- data/docs/reference/dsl.md +19 -5
- data/docs/reference/errors.md +6 -3
- data/docs/reference/generated-artifacts.md +29 -2
- data/docs/reference/public-api.md +7 -2
- data/docs/reference/regexp.md +8 -6
- data/docs/reference/runtime.md +20 -3
- data/docs/reference/tokens-and-locations.md +6 -4
- data/docs/releases/v1.1.0.md +44 -0
- data/lib/flexr/action_resolver.rb +15 -0
- data/lib/flexr/artifact_writer.rb +61 -0
- data/lib/flexr/automaton/accel.rb +9 -4
- data/lib/flexr/automaton/analysis.rb +47 -6
- data/lib/flexr/automaton/backend_cost_model.rb +40 -0
- data/lib/flexr/automaton/compiler.rb +50 -71
- data/lib/flexr/automaton/dfa.rb +106 -19
- data/lib/flexr/automaton/minimizer.rb +79 -31
- data/lib/flexr/automaton/nfa.rb +45 -13
- data/lib/flexr/automaton/types.rb +14 -0
- data/lib/flexr/cli.rb +11 -5
- data/lib/flexr/codegen/direct.rb +5 -41
- data/lib/flexr/codegen/table.rb +175 -69
- data/lib/flexr/codegen.rb +8 -0
- data/lib/flexr/configuration.rb +37 -0
- data/lib/flexr/dsl.rb +105 -39
- data/lib/flexr/errors.rb +1 -0
- data/lib/flexr/generated.rb +61 -35
- data/lib/flexr/generator.rb +47 -82
- data/lib/flexr/importer.rb +4 -40
- data/lib/flexr/options.rb +5 -3
- data/lib/flexr/rake_task.rb +6 -1
- data/lib/flexr/regexp/ast.rb +5 -2
- data/lib/flexr/regexp/normalizer.rb +62 -16
- data/lib/flexr/regexp/parser.rb +121 -46
- data/lib/flexr/regexp/tokenizer.rb +192 -67
- data/lib/flexr/runtime/buffer.rb +115 -9
- data/lib/flexr/runtime/core.rb +214 -47
- data/lib/flexr/runtime/errors.rb +64 -4
- data/lib/flexr/runtime/interpreter.rb +178 -71
- data/lib/flexr/runtime.rb +70 -0
- data/lib/flexr/source/passthrough.rb +62 -9
- data/lib/flexr/source/prism_reader.rb +175 -46
- data/lib/flexr/source/static_eval.rb +40 -4
- data/lib/flexr/source.rb +6 -0
- data/lib/flexr/unicode/data/properties.rb +1 -1
- data/lib/flexr/unicode/data.rb +11 -0
- data/lib/flexr/unicode/property.rb +3 -5
- data/lib/flexr/unicode/reference_regexp.rb +4 -0
- data/lib/flexr/unicode/version.rb +7 -0
- data/lib/flexr/version.rb +1 -1
- data/lib/flexr.rb +9 -76
- data/tools/coverage.rb +6 -1
- metadata +12 -1
data/lib/flexr/regexp/parser.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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?("-")
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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" => [[
|
|
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:
|
|
414
|
+
return AST::Anchor.new(kind: :bol, loc: span(starting)) if char == "^"
|
|
344
415
|
|
|
345
|
-
AST::Anchor.new(kind: :eol, loc:
|
|
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
|