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
|
@@ -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 = @
|
|
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 @
|
|
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 @
|
|
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
|
-
|
|
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 = @
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 =
|
|
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 = @
|
|
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(
|
|
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 = @
|
|
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
|
-
|
|
193
|
+
def fast_path?(machine)
|
|
194
|
+
@fast_paths ||= {}
|
|
195
|
+
return @fast_paths[machine.dfa] if @fast_paths.key?(machine.dfa)
|
|
140
196
|
|
|
141
|
-
rules = @
|
|
142
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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(
|
|
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(
|
|
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 =
|
|
204
|
-
|
|
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.
|
|
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 = @
|
|
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
|
-
@
|
|
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 = @
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
return match_end if match_end && buffer.eof_loaded?
|
|
451
|
-
return unless buffer.ensure_available?(buffer.bytesize + 1)
|
|
528
|
+
end
|
|
452
529
|
|
|
453
|
-
|
|
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 @
|
|
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 = @
|
|
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 = @
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
26
|
+
replaced_size = [end_offset, insertion].min - start_offset
|
|
27
|
+
replacement.bytesize - replaced_size
|
|
17
28
|
end
|
|
18
|
-
|
|
19
|
-
result
|
|
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(
|
|
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
|